OVM/OVM методология/Механика OVM
Содержание |
Механика OVM
Библиотека OVM предоставляет много возможностей для построения testbenches. В этой главе мы ознакомимся с наиболее важными возможностями, которые вы будете использовать почти во всех ваши testbenches.
Компоненты и иерархия
Первичная структура для создания элементов testbench является компонента. Компонента в OVM – это аналогия модулю в Verilog. Компонента OVM сконструирована на основе класса, который обеспечивает ему различные характеристики, чем модуль Verilog и имеет различные использования импликаций. Среди таких характеристик является то, что классы создаются во время выполнения, а не во время разработки, как модули. Таким образом, OVM отвечает за создание компонента и сборки их в иерархии.
![]() |
Рисунок 4-1 иллюстрирует простую иерархию компонент. После мы покажем, как построить эту иерархию с помощью средств OVM для создания компонент и объединения их в иерархии:
Самый верхний узел, ENV, является корнем. Корень отличается тем, что он не имеет предка. Все остальные узлы имеют только одного предка. Каждый узел имеет имя. Расположение каждого узла в иерархии может быть идентифицировано уникальным full_name (путем), который строится на нанизывании имен всех узлов между корнем и узлов в запросе, разделяя их сепаратором иерархии, точкой (.). Например, путь к компоненту, второго потомка с2 - top.c2.child2.
Компонент в ОВМ является классом, производным от ovm_component. Простейшая компоненты - листья, те, которые не имеют потомков.
57 class child extends ovm_component; 58 59 function new(string name, ovm_component parent); 60 super.new(name, parent); 61 endfunction 62 63 endclass file: 04_OVM_mechanics/01_hierarchy/top.sv
Конструктор имеет два параметра, имя компоненты и указатель на его предка. Название - простое имя, а не иерархический путь. Предок предоставляет место для подключения новой компоненты в иерархии. Полный путь потомка создается путем объединения имени потомка с полным именем предка, разделенный точкой (.).OVM предоставляет методы для получения и имени и полного пути компоненты:
string get_name(); string get_full_name();
Подчиненные компоненты создаются в функции build(), которая вызывается на этапе сборки (фазы объясняется далее в этой главе). Создание экземпляра компонента включает вызов new() для выделения памяти для нее и передачи соответствующих параметров в конструктор. В компоненте, показанной ниже, мы создаем две подчиненные компоненты, и child1 child2.
71 class component extends ovm_component; 72 73 child child1; 74 child child2; 75 76 function new(string name, ovm_component parent); 77 super.new(name, parent); 78 endfunction 79 80 function void build(); 81 child1 = new(“child1”, this); 82 child2 = new(“child2”, this); 83 endfunction 84 85 endclass file: 04_OVM_mechanics/01_hierarchy/top.sv
Как component, ENV также создает экземпляры двух подчиненных компонент, c1 и c2. Вся иерархия с корнем в модуле, называемым top в нашем проекте.(??)
131 module top; 132 133 env e; 134 135 initial begin 136 e = new(“env”); 137 run_test(); 138 end 139 140 endmodule file: 04_OVM_mechanics/01_hierarchy/top.sv
Вызов new() создает верхний уровень окружающей среды. run_test () начинает выполнение testbench.
В SystemVerilog, модули, интерфейсы и программные блоки создаются во время разработки, в то время как классы создаются после разработки, во время выполнения. Таким образом, чтобы создать иерархию классов, мы должны иметь интерфейс, модуль или программу, который содержит начальный блок, который начинает процесс создание компонент иерархии, основанных на классах. Интерфейсы предназначены для использования в качестве средства связи между двумя модулями и не очень хорошо подходят для использования в качестве корня иерархии классов. Программные блоки или модули могут быть использованы как корень (??). Для нашей простой иерархии, это не имеет значения. Позже, когда мы подключаем класс-компонент для модуля на основе аппаратных средств, мы увидим, что использование модуля предпочтительнее программным блокам.
Перемещение по иерархии
Мы можем исследовать структуры данных, используемые для реализации компонента иерархии с некоторыми методами, предусмотренными в ovm_component. Потомки компоненты хранятся в ассоциативном массиве. Этот массив напрямую не доступен, но он может быть доступен через иерархию API. Этот API-интерфейс похож на встроенные методы SystemVerilog, предназначенные для ассоциативных массивов.
int get_first_child(ref string name); int get_next_child(ref string name); ovm_component get_child(string name); int get_num_children();
get_first_child () и get_next_child () работают вместе для перебора множества потомков, содержащихся в компоненте. get_first_child () извлекает имя первого ребенка в списке. Он возвращает имя в качестве ссылки. get_next_child () возвращает имя следующего ребенка в списке. Он возвращает 1, если имеется следующая имя потомка и 0, если был достигнут конец списка. get_child () преобразует имя в ссылку компоненты.
Используя эти функции, мы можем пройти по компонентам иерархии.
73 function void depth_first(ovm_component node, 74 int unsigned level = 0); 75 76 string name; 77 78 if(node == null) 79 return; 80 81 visit(node, level); 82 83 if(node.get_first_child(name)) 84 do begin 85 depth_first(node.get_child(name), level+1); 86 end while(node.get_next_child(name)); 87 88 endfunction file: 04_OVM_mechanics/utils/traverse.svh
Эта функция будет осуществляющая обход в глубину иерархии, называя visit(). Мы используем get_first_child () и get_next_child () для перебора списка каждого потомка в каждом узле. Для каждого итерации мы вызываем depth_first () рекурсивно. Для нашего маленького проекта, результат будет следующий:
+ env | + env.c1 | | env.c1.child1 | | env.c1.child2 | + env.c2 | | env.c2.child1 | | env.c2.child2
Функция visit() использует глубину узла и определяет является ли он конечным для печати строки каждого узла.
Singleton Top
Компоненты, которые не имеют предков (то есть, параметр предок в конструкторе является нулевым) называют orphans.(??) В OVM вы можете создать столько компоненты без предков, сколько вам необходимо. Тем не менее, не существует такого понятия, как истинные orphans. Любой компонент, чей предок является null, относится к встроенному предку, называемому ovm_top. ovm_top состоит из одного экземпляра ovm_root. Это предок всех компонент, которые имеют предков. В самом деле, env в нашем предыдущем примере является потомком ovm_top. Так как он не имеет предка, то автоматически получают ovm_top в качестве родителя.
singleton - широко известный объектно-ориентированных шаблон проектирования, который имеет private (local) или protected конструкторы и статическую функцию get, которая возвращает тот же самый указатель не смотря на то, сколько раз она вызывается. Это означает, что возможно существование одного экземпляра, и что экземпляр создается автоматически при вызове функции get. ovm_top содержит дескриптор экземпляра singleton ovm_root(???). Он статически инициализируется при вызове ovm_root :: Get (). Вы можете вызвать ovm_root :: Get () в любое время, но в этом нет необходимости, так как ovm_top предусматривает это.
Есть целый ряд полезных следствий существования singleton top-level компоненты. Одним из них является, что вы можете достичь любую компоненту из ovm_top. Если вы запустили алгоритм обхода иерархии, начиная с ovm_top, вы доберетесь до каждой компоненты в системе. Другим следствием является то, что любая компонента, включая порты, экспорт и каналы, которые создаются внутри модуля, достижима из ovm_top. Если вы хотите изменить отчет во всех обработчиках компонент, вы можете сделать это, вызвав одну из иерархических функции отчетности в ovm_top. ovm_top содержит все механизмы синхронизации, которые объясняются далее в этой главе.
Connectivity
Компоненты соединены друг с другом через TLM port и exports. Port и exports обеспечивают средства для компонент, или, точнее, процессы в компонентах для синхронизации и связи друг с другом. Port и exports - объекты, которые образуют пункт связывания для обеспечения межкомпонентной связи. Как уже говорилось в предыдущей главе, exports обеспечивают функции и задачи, которые могут быть вызваны из Port.
Метод connect портов и exports используется для связывания их вместе.
initiator_port.connect(target.export)
Этот метод создает ассоциацию, или связывание, между port и export так, что port может вызывать(запускать) задачи и функции из export. Для успешного соединения, типы port и export должны совпадать. То есть, типы интерфейсов должны быть одинаковыми, и тип объекта, передающегося в интерфейс должны быть одинаковыми.
Подключение через Иерархию
Как и контактами в RTL дизайне, нам нужно подключиться к портам TLM через иерархические границы. Рисунок 4-3 использует простой дизайн, чтобы показать, как сделать эти соединения. Этот проект содержит исходную компоненту с двумя port(выходами), которые в конечном итоге подключаются к двум входам(export). Для осуществления подключения между этими компонентами, мы должны расширить port и export на уровень выше в иерархии.
Компонент source содержит два порта, и first_put_port, second_put_port.
65 class source extends ovm_component; 66 67 ovm_put_port #(trans_t) first_put_port; 68 ovm_put_port #(trans_t) second_put_port; 69 70 function new(string name, ovm_component parent); 71 super.new(name, parent); 72 endfunction 73 74 function void build(); 75 first_put_port = new(“first_put_port”, this); 76 second_put_port = new(“second_put_port”, this); 77 endfunction file: 04_OVM_mechanics/02_connectivity/top.sv
Кроме того, компонента sink создает экспорт и создает его в функции build. Экспорт связан с внутренним каналом, FIFO, от которого компонента может получать объекты во время выполнения.
126 class sink extends ovm_component; 127 128 ovm_put_export #(trans_t) put_export; 129 local tlm_fifo #(trans_t) fifo; 130 131 function new(string name, ovm_component parent); 132 super.new(name, parent); 133 endfunction 134 135 function void build(); 136 put_export = new(“put_export”, this); 137 fifo = new(“fifo”, this); 138 endfunction 139 140 function void connect(); 141 put_export.connect(fifo.put_export); 142 endfunction file: 04_OVM_mechanics/02_connectivity/top.sv
source_wrapper должен создать соединение между внутренним источником компоненты и внешней ее границей. Он делает это соединение, создавая свои порты, которые имеют тот же тип, что и тип портов lower-level, в нашем случае, тех, которые принадлежат к source.
98 class source_wrapper extends ovm_component; 99 100 source s; 101 ovm_put_port #(trans_t) put_port1; 102 ovm_put_port #(trans_t) put_port2; 103 104 function new(string name, ovm_component parent); 105 super.new(name, parent); 106 endfunction 107 108 function void build(); 109 s = new(“source”, this); 110 put_port1 = new(“put_port1”, this); 111 put_port2 = new(“put_port2”, this); 112 endfunction 113 114 function void connect(); 115 s.first_put_port.connect(put_port1); 116 s.second_put_port.connect(put_port2); 117 endfunction 118 119 endclass file: 04_OVM_mechanics/02_connectivity/top.sv
160 class sinker extends ovm_component; 161 162 ovm_put_export #(trans_t) first_put_export; 163 ovm_put_export #(trans_t) second_put_export; 164 165 sink sink1; 166 sink sink2; 167 168 function new(string name, ovm_component parent); 169 super.new(name, parent); 170 endfunction 171 172 function void build(); 173 sink1 = new(“sink1”, this); 174 sink2 = new(“sink2”, this); 175 first_put_export = new(“first_put_export”, this); 176 second_put_export = new(“second_put_export”, this); 177 endfunction 178 179 function void connect(); 180 first_put_export.connect(sink1.put_export); 181 second_put_export.connect(sink2.put_export); 182 endfunction 183 184 endclass file: 04_OVM_mechanics/02_connectivity/top.sv
Два нижних уровня компоненты sink и exports создаются обычным способом. Затем они соединяются с помощью метода подключения export. Сейчас мы создаем port-export соединение между source_wrapper и sinker, также с помощью функции Connect.
192 class env extends ovm_component; 193 194 sinker s; 195 source_wrapper sw; 196 197 function new(string name, ovm_component parent = null); 198 super.new(name, parent); 199 endfunction 200 201 function void build(); 202 s = new(“sinker”, this); 203 sw = new(“source_wrapper”, this); 204 endfunction 205 206 function void connect(); 207 sw.put_port1.connect(s.first_put_export); 208 sw.put_port2.connect(s.second_put_export); 209 endfunction 210 211 task run; 212 global_stop_request(); 213 endtask 214 215 endclass file: 04_OVM_mechanics/02_connectivity/top.sv
Для новых пользователей часто может вызвать трудности определить, порт или экспорт необходимо подключить и какой объект является аргументом. Вы можете легко понять это, следуя потоку управления через систему. Общее правило заключается в том, что запрашиваемый порт вызывается с помощью функции connect() и передачи в нее запрашиваемого port или export в качестве аргумента. Рисунок 4-4 показывает поток управления через иерархическую систему.
Порт – объект вызова и экспорт - объект вызванной функции или задачи. Вы можете воспринимать порты, как вызов экспорта. Так, в env, мы вызываем connect на put_ports с put_exports в качестве аргументов. Для port-to- port и export-to-export иерархии соединений, вызывающий будет чуть менее очевиден. Если вызов осуществляется со стороны порта, вы можете считать, что вызов lowest-level порта в иерархии является вызовом методов интерфейса upper-level порта. Аналогично, если экспорт объект вызова, вы можете считать upper-level экспорт вызовом lower-level export. В таблице ниже приведены возможные типы подключения:
Примечание для пользователей AVM
В AVM-3.0 соединения были сделаны аналогично: вызовам функции connect() из port и export. Кроме того, export-to-export, port-to-export, и port-to-port вызовы были сделаны на разных фазах, export_connections (), Connect (), и import_connections (), соответственно. В OVM, порядок, в котором вызывается connect(собственно происходит подключение), не важен, он может быть вызван в любом порядке. Мы рекомендуем вам создавать подключения в отдельных функциях и их вызов производить в нужное для вас время (к примеру можно назвать функцию connect(), не следует путать с методом connect(), который содержится в входных и выходных портах). OVM поддерживает позднее связывание, (a feature where calls to connect() only make a note that a connection is to be made..) фишка в том что соединение появится только тогда когда произведем вызов функции создания подключения. Позже, перед end_of_elaboration, notes соединения будут согласованы и выполнены. Это позволяет чище использовать модель и избавит от большинства очевидных ошибок подключение при соединении компонент. ( much more forgiving of understandable errors where connect() calls were made in the wrong order.)
Фазы
Для традиционных модулей Verilog полная разработка проекта и его выполнение осуществялется в симуляторе. Так как OVM компоненты - это классы, они создаются и подключаются, и их выполнение начинается за пределами Verilog разработчика. Компоненты создаются вызовом конструктора класса операцией new(), которая выделяет память и выполняет инициализацию. В то время как Verilog время выполнения run-time engine managing instantiation, разработка, и выполнение компонент на основе классов, функциональность компонент разделена на фазы, и контроллер фазы OVM управляет их исполнением.
Каждая фаза представлена в компоненте как виртуального метода (задачи или функция) с тривиальной реализацией по умолчанию. Эти phase callbacks реализуются разработчиком компоненты, который обеспечивает соответствующую функциональность. Фаза контроллера гарантирует, что фазы выполняются в надлежащем порядке. Набор стандартных фаз показано в следующей таблице:
Каждая фаза имеет определенную цель. разработчики Компонент должны заботиться о том, чтобы функциональность реализованная в каждой фазе callback соответствовала фазе определения.
• New технически не является фазой, в том, что она не управляется фазой контроллера. Тем не менее, для каждого компонента, конструктор должен быть выполнен и завершен в целях создания компоненты. Таким образом, new должен выполниться до build() или любого другого последующего этапах выполнения.
• build это место, где новые компоненты, порты и экспорт создаются и настраиваются. Это также рекомендуемое место для вызова set_config_ * и get_config_ * (см. Section4.4).
• connect – место, где компоненты, порты и экспорт созданные в build() связываются.
• end_of_elaboration здесь вы можете выполнить изменение конфигураций, зная, что разработка уже завершена. То есть, вы можете предположить, что все компоненты созданы и соединены.
• start_of_simulation выполняется только до времени 0.
• run pre-defined task фазой. Все выполняемые задачи работают параллельно. Каждая выполняемая задача продолжается, пока ее управление не достигнет оператора EndTask или она явно будет прекращена. Позже в этой главе мы обсудим, как завершать testbenches.
• extract предназначен для сбора информации, касающейся покрытия или другой информации о том, как ответить на вопросы testbench.
• проверка - место, где выполняется любая проверка правильности или проверка валидации извлеченных данных.
• отчет -место , где делаются окончательные отчеты.
Ниже простой пример, использующий ovm_report_info () для иллюстрации порядка выполнения фаз.
38 class sub_component extends ovm_component; 39 40 function new(string name, ovm_component parent); 41 super.new(name, parent); 42 endfunction 43 44 function void build(); 45 ovm_report_info(“build”, ““); 46 endfunction 47 48 function void connect(); 49 ovm_report_info(“connect”, ““); 50 endfunction 51 52 function void end_of_elaboration(); 53 ovm_report_info(“end_of_elaboration”, ““); 54 endfunction 55 56 function void start_of_simulation(); 57 ovm_report_info(“start_of_simulation”, ““); 58 endfunction 59 60 task run(); 61 ovm_report_info(“run”, ““); 62 endtask 63 64 function void extract(); 65 ovm_report_info(“extract”, ““); 66 endfunction 67 68 function void check(); 69 ovm_report_info(“check”, ““); 70 endfunction 71 72 function void report(); 73 ovm_report_info(“report”, ““); 74 endfunction 75 76 endclass file: 04_OVM_mechanics/03_phases/top.sv
В компоненте верхнего уровня, мы создаем две компоненты, каждая из которых в свою очередь создает две sub_component. sub_components являющимися такими же, как и сами компоненты; каждая фаза обратного callback просто печатает строку, определяющую фазу. При выполнении, вы получите следующий результат:
OVM_INFO @ 0 [RNTST] Running test ... OVM_INFO @ 0: env.c1 [build] OVM_INFO @ 0: env.c1.s1 [build] OVM_INFO @ 0: env.c1.s2 [build] OVM_INFO @ 0: env.c2 [build] OVM_INFO @ 0: env.c2.s1 [build] OVM_INFO @ 0: env.c2.s2 [build] OVM_INFO @ 0: env.c1.s1 [connect] OVM_INFO @ 0: env.c1.s2 [connect] OVM_INFO @ 0: env.c1 [connect] OVM_INFO @ 0: env.c2.s1 [connect] OVM_INFO @ 0: env.c2.s2 [connect] OVM_INFO @ 0: env.c2 [connect] OVM_INFO @ 0: env.c1.s1 [end_of_elaboration] OVM_INFO @ 0: env.c1.s2 [end_of_elaboration] OVM_INFO @ 0: env.c1 [end_of_elaboration] OVM_INFO @ 0: env.c2.s1 [end_of_elaboration] OVM_INFO @ 0: env.c2.s2 [end_of_elaboration] OVM_INFO @ 0: env.c2 [end_of_elaboration] OVM_INFO @ 0: env.c1.s1 [start_of_simulation] OVM_INFO @ 0: env.c1.s2 [start_of_simulation] OVM_INFO @ 0: env.c1 [start_of_simulation] OVM_INFO @ 0: env.c2.s1 [start_of_simulation] OVM_INFO @ 0: env.c2.s2 [start_of_simulation] OVM_INFO @ 0: env.c2 [start_of_simulation] OVM_INFO @ 0: env.c2 [run] OVM_INFO @ 0: env.c2.s2 [run] OVM_INFO @ 0: env.c2.s1 [run] OVM_INFO @ 0: env.c1 [run] OVM_INFO @ 0: env.c1.s2 [run] OVM_INFO @ 0: env.c1.s1 [run] OVM_INFO @ 1: env.c1.s1 [extract] OVM_INFO @ 1: env.c1.s2 [extract] OVM_INFO @ 1: env.c1 [extract] OVM_INFO @ 1: env.c2.s1 [extract] OVM_INFO @ 1: env.c2.s2 [extract] OVM_INFO @ 1: env.c2 [extract] OVM_INFO @ 1: env.c1.s1 [check] OVM_INFO @ 1: env.c1.s2 [check] OVM_INFO @ 1: env.c1 [check] OVM_INFO @ 1: env.c2.s1 [check] OVM_INFO @ 1: env.c2.s2 [check] OVM_INFO @ 1: env.c2 [check] OVM_INFO @ 1: env.c1.s1 [report] OVM_INFO @ 1: env.c1.s2 [report] OVM_INFO @ 1: env.c1 [report] OVM_INFO @ 1: env.c2.s1 [report] OVM_INFO @ 1: env.c2.s2 [report] OVM_INFO @ 1: env.c2 [report]
Вы видите, что build()выполняется сверху вниз и остальные фазы запуска снизу вверх. Вы также можете видеть, что каждый этап завершается во всех компонентах до начала следующего этапа. Таким образом, в connect(), например, вы можете расчитывать, что build() завершена во всех компонентах. Вы также заметите, что время запускается после фазы run. В нашем примере задача run - тривиальна, она просто выполняет задержку времени на единицу (# 1).
un_test (), упоминалось ранее в разделе 4.1, инициирует фазу выполнение. Он начинает выполнение фаз по порядку и контролирует, что каждый этап завершен до начала следующего.
Config
Для увеличения повторного использования компонент, желательно объявит их с параметрами, которые могут быть внешне настроены. Конфигурационные средства обеспечивают возможности для этого. Они основаны на базе пар имя-значение, и называются configuration items, которые организованы иерархически. Каждая компонента содержит таблицу конфигураций элементов конфигурации, так как компоненты организованы в качестве дерева, каждый элемент в базе данных может быть уникально размещен по расположению компоненты и имени элемента конфигурации
Класс ovm_component содержит два набора методов для конфигурации размещения элементов в базу данных и потом для их получения. Это set_config_ * и * get_config_. В таблице ниже приведены оба набора.
Set_config_ * функция помещает элемент в конфигурационную базу данных текущего компонента, то есть в экземпляре компонента, в котором функция вызывается. Каждая из этих функций принимают три аргумента, имя, field_name и значение. Аргумент name - имя пути, который представляет набор компонент, которые должны соответсвовать кофигурациям этого элемента. name используется в * get_config_, чтобы найти элементы в базе данных конфигурации. field_name - это имя поля и должно быть уникальным в пределах текущей базы конфигураций. value является частью пары имя-значение и его тип может быть строка, INT, или ovm_object, в зависимости от вызываемой функции. Кроме того, set_config_object принимает копию аргумента для указания, был ли объект передан в качестве значения так как значение должно быть клонировано прежде чем оно будет помещено в базу конфигурации. Функции get_config_ * извлекает элементов из базы данных конфигураций. Эти функции принимают только два аргумента, имя поля и INOUT переменную, который содержит значение расположения элемента. Они также возвращают бит, чтобы указать был ли удачно расположен запрашиваемый объект. * Get_config_ функции не принимают аргументы имени пути, как их set_config_ * коллеги, поскольку они используют путь к текущему компоненту, как ссылку, чтобы найти элементы конфигурации. Они разработаны, чтобы узнать, есть значение элемента конфигурации для текущего контекста, то есть, компонент, в котором функция get_config_ * вызывается.
Алгоритм поиска для извлечения элементов конфигурации использует имя пути компонента запроса и путь к файлу вставляемый в каждый item. Он начинается с поиска конфигурационных элементов в базе данных в верхней компоненте по field_name. Если такой элемент существует, то он затем спрашивает, соответствует ли указанный путь к файлу пути имени компоненты. Если элемента с указанным field_name нет или имена путей не совпадают, то поиск идет с дочерней компоненты. Этот процесс продолжается до соответствия или до достижения компоненты начала поиска.
Путь каждого элемент конфигурации может быть регулярным выражением. Таким образом, мы используем алгоритм соответствия регулярных выражений для нахождения совпадения с путем запрошенной компоненты и именем пути элемента конфигурации. Эффект заключается в соответствии иерархической области. В качестве примера, рассмотрим простую иерархию на рисунке 4-5. Давайте предположим, что в env::build()мы запускаем два вызова set_config_ *:
112 function void build(); 113 c1 = new(“c1”, this); 114 c2 = new(“c2”, this); 115 116 set_config_int(“c2.*”, “i”, 42); 117 set_config_int(“*”, “t”, 19); 118 endfunction file: 04_OVM_mechanics/04_config/top.sv
Это вызовет запись двух конфигурационных элементов в базу данных в env. Обратите внимание на звездочку (*) в имени пути. Имя пути в вызове set_config_ * - регулярное выражение, и дикие символы используются для указать несколько областей, к которыми элемент применяется. По элементу i, c2. * показывает, что в любой области ниже c2 в иерархии, i будет определять указанное значение. В этом случае указанное значение - 42. Если вы опустите звездочку, то элемент конфигурации применяется только к c2, а не к любому из своих потомков.
Состояние конфигурации базы данных для каждого компонента в иерархии после вызова set_config_ * показано на рисунке 4-6,
пусть в элементе top.c1.child1 мы осуществим вызов:
int i; ... get_config_int(“i”, i)
Поиск задает вопрос: Каково значение конфигурации i в иерархических рамках top.c1.child1? Чтобы ответить на этот вопрос, конфигурации базы данных в env ищется в первую очередь. Запись i там говорит, что значение i совпадающее с env.c2. * - 42. Тем не менее, компоненты, из которых был сделан запрос, находятся в c1 sub-hierarchy. Таким образом, не существует соответствия, а get_config_int () возвращает статус неудачи. Запрос в любую компоненту, которая является дочерней c2 будет успешно завершен и вернется значение 42.
Ниже приведен код для функцию build() дочерней компоненты. Это место где эти компоненты ищут конфигурационные значения i и t.
60 function void build(); 61 62 string msg; 63 64 if(!get_config_int(“t”, t)) begin 65 $sformat(msg, “no value for t found in config database, using default value of %0d”, t); 66 ovm_report_warning(“build”, msg); 67 end 68 69 if(!get_config_int(“i”, i)) begin 70 $sformat(msg, “no value for i found in config database, using default value of %0d”, i); 71 ovm_report_warning(“build”, msg); 72 end 73 74 endfunction file: 04_OVM_mechanics/04_config/top.sv
Следующий пример показывает вывода при выполнении этого проекта:
# OVM_INFO @ 0 [RNTST] Running test ... # OVM_WARNING @ 0: env.c1.child1 [build] no value for i found in config database, using default value of 91 # OVM_WARNING @ 0: env.c1.child2 [build] no value for i found in config database, using default value of 91
Запрос на конфигурационный элемент t был успешным во всех контекстах с того момента как вызов set_config_int установил, что tдоступно во всех контекстах. Два запроса на элемент конфигурации i были успешными, и два нет. Результат получился такой потому что мы ограничены наличием i только компонентах на уровне или ниже env.c2. Компоненты на уровне или ниже c1 не могут видеть элемент конфигурации i из-за того, как мы создали базу данных конфигурации.
Конфигурация и фазы
Теперь, когда мы знаем, что набор вызовов для размещения и извлечение элементов в конфигурацию базы данных, следующей задачей, является эффективное применение этих функции для настройки компонент. Конфигурация может быть использована для изменения поведения или структуры testbench. Как правило, поведенческий и структурный режимы определяются, когда testbench начинает работу, поэтому он является наиболее удобным местом для установки параметров конфигурации на одной из ранних стадий, таких как new, build или connect.
Из таблицы фаз, вы можете увидеть, что new и build являются нисходящими фазами, в то время как все остальные фазы восходящие. Так что, если вы хотите установить элемент конфигурации в базу данных на более высокий уровень контекста, чтобы он был подхвачен более низким уровнем, вы должны вызвать функцию set_config_ * в new или build фазе. Фазы выполняется дискретно, это означает, что каждая фаза выполняется до начала следующей. Вы можете установить элементы конфигурации в new или build фазу и извлекать их для использования в установке поведенческого режима или изменения топологии на этапе build. В вашей функции build, сначала вызовите get_config_ * для извлечения элементов из более высоких уровней иерархии для управления конфигурацией текущего уровня. Затем добавьте вызов set_config_ * для помещения элементов конфигурации в базу данных для использования компонентами на более низких уровнях иерархии. Наконец, используя соответствующие элементы конфигурации, обработайте компоненты. Важно вызвать get_config_ * вначале, потому, что указанная информация может влиять на значения, которые установлены на более низких уровнях иерархии.
Например, настройка топологии включает в себя установку параметров топологии на верхнем уровне среды, а затем применение эти параметров в различных компонентах, которые находятся ниже верхнего уровня среды в иерархии. Наш пример имеет шину, которая может иметь любое количество masters или slaves. Число masters и slaves расположены в верхнем уровне среды. Модель шину подхватывает эту конфигурационную информацию и использует ее для построения шины. В build функции верхнего уровня окружающей среды, мы создаем модель шины и настраиваем ее с числом masters и slaves.
129 function void build(); 130 set_config_int(“bus”, “masters”, 4); 131 set_config_int(“bus”, “slaves”, 8); 132 b = new(“bus”, this); 133 endfunction file: 04_OVM_mechanics/05_config_topo/top.sv
Модель шины построена таким образом, что число masters и slaves не фиксировано. Вместо этого, количество приходит из конфигурации системы.
90 function void build(); 91 92 int unsigned i; 93 94 if(!get_config_int(“masters”, masters)) begin 95 $sformat(msg, “\”masters\” is not in the configuration database, using default value of %0d”, masters); 96 ovm_report_warning(“build”, msg); 97 end 98 99 for(i = 0; i < masters; i++) begin 100 $sformat(name, “master_%0d”, i); 101 m = new(name, this); 102 end 103 104 if(!get_config_int(“slaves”, slaves)) begin 105 $sformat(msg, “\”slaves\” is not in the configuration database, using default value of %0d”, slaves); 106 ovm_report_warning(“build”, msg); 107 end 108 109 for(i = 0; i < slaves; i++) begin 110 $sformat(name, “slave_%0d”, i); 111 s = new(name, this); 112 end 113 114 endfunction file: 04_OVM_mechanics/05_config_topo/top.sv
В функции build для модели шины, схема получает необходимую конфигурационную информацию, используя вызовы get_config_int. В каждом случае, Возвращаемое значение проверяется, чтобы определить, была ли получена запрошенная конфигурационная информация. Если нет, сообщение об ошибке предупреждает, что конфигурационный элемент не найден и что будет использовано значение по умолчанию. С точки зрения Best Practices, важно убедиться, что возвращается значение, проверено и выдается предупреждение, в случае ошибки. Без этой проверки, то, что используется значения по умолчанию, могло остаться незамеченным. В некоторых случаях это может быть приемлемо, в других случаях - нет. Человек, создающий модель шины не может знать всех обстоятельств, при которых модель будет использоваться. Поэтому важно сделать все возможное, чтобы модель была надежной. Проверка возвращаемых значений и выдача соответствующих сообщений является одним из способов повышения надежности модели.
В цикле, где мы строим masters, ссылки на каждого нового master сохраняются в той же переменной, m. Каждый новый master заменяет предыдущий. Мы не используем массив для хранения всех компонент. В каждой итерации, мы используем $ sformat, чтобы создать уникальное имя. Конструктор, new(), вызывает super.new (), конструктор в ovm_component базовом классе, который отвечает за вставку вновь созданную компоненту потомка в родительский список. Нет необходимости явно сохранять компоненту, потому что родительская компонента сделает это за нас. Цикл для создания slaves организован таким же образом.
Фабрика
Структура testbench определяется организацией компонент в иерархии и то, как эти объекты связаны между собой. Поведение testbench определяется процедурным кодом в фазе callbacks—build, connect, run и так далее. Есть моменты, когда это необходимо изменить поведение или часть структуры внешне, то есть во время выполнения, не касаясь, кода testbench. Например, чтобы ввести ошибку в систему, вы можете заменить обычный драйвер драйвером с ошибкой, который умышленно вводит ошибки. Вместо повторного кодирования окружающей среды для использования другого драйвера, вы можете использовать фабрику, чтобы сделать замену автоматически.
Фабрика обеспечивает средства для замены одного объекта на другой без использования текстового редактора для изменения testbench. Вместо создания объект с помощью new(), вы вызываете функцию create фабрики. Фабрика хранит список зарегистрированных объектов и, при необходимости, набор переопределения связанный с каждой из них. При создании объекта с помощью фабрики, список переопределения проверяется на наличие такого объекта. Если он есть, то переопределенный объект возвращается. В противном случае, зарегистрированный объект возвращается.
Фабрика является OVM структурой данных. Она глобальна в области видимости, и только один элемент может существовать (то есть, это singleton). Она служит в качестве полиморфных конструкторов, одна функция, которая позволяет создавать различные объекты. Она обеспечивает средства для регистрации объектов, и для их переопределения. Объекты, зарегистрированные как переопределения, должны быть производным от объекта, от которого они переопределялись. Для того, чтобы одна функция возвращала несколько объектов, каждый из этих объектов должны быть получен от общего базового класса. Важным компонентом фабрики является оболочка, класс, который описывает объект, который мы хотим создать с помощью фабрики. Структура данных фабрики основывается на таблице оболочек, проиндексированной по ключу. Оболочка имеет функцию create(), которая обращается к конструктору обернутого объекта.
Использование фабрики состоит из трех этапов: регистрация, настройки переопределение и создание. На первом этапе необходимо зарегистрировать объект с помощью фабрики. На втором шаге, добавляется переопределение зарегистрированного объекта. На третьем этапе, вы создаете объект с помощью фабрики, который будет возвращать либо первоначально зарегистрированный объект или переопределенный, в зависимости от того, переопределение было передано в качестве запрашиваемого объекта.
Как работает фабрика
Термин фабрика был придуман The Gang of Four для использования в программном обеспечении в их книге Design Patterns: Elements of Reusable Object-Oriented Software. В этой книге, они определили шаблон, который они называют abstract factory, как интерфейс для создания семейств связанных объектов. Они определили шаблон factory method в качестве интерфейса для создания объектов, относящихся к подклассам для определения, какой объект создавать. Фабрика OVM является комбинацией этих двух шаблонов. Она предоставляет средства для создания семейства объектов, а также предоставляет средства для определения, какой именно объект создавать структуре данных фабрики.
Фабрика OVM основана на структуре данных, которая отображает необходимые типы в переопределенные. По сути, организация представляет собой ассоциативный массив типов, ключом в котором является также тип. При регистрации типа при помощи фабрики, его переопределенным типом является он сам. Таким образом, по умолчанию, когда вы запрашиваете объект этого типа, вы получаете только тип. Фабрика также предоставляет средства для замены переопределенных типов другими типами, чтобы вы могли получить переопределенные типы, которые отличаются от зарегистрированных типов.
Следующий пример является сильно упрощенной фабрикой, которая показывает, как OVM фабрика работает. Фабрика сохраняет основные структуры OVM фабрики, но многие детали были удалены для более понятной иллюстрации. Наша фабрика реализуется в четырех классах, двух базовых классах и двух производных классах, которые делают реальную работу. Двумя базовыми классами являются object_base и wrapper_base. Все объекты, зарегистрированные в фабрике, должны быть получены (в конечном счете) от object_base, а wrapper_base является базовым классом для параметризованной обертки. Factory - это singleton, который содержит ассоциативный массив, содержащий экземпляры обертки. Наконец, обертки являются производными от wrapper_base и параметризованными классами, которые представляют уникальные типы.
Для нашей фабрики, базовые классы тривиальны:
46 class object_base; 47 virtual function void print(); 48 $display(“object_base”); 49 endfunction 50 endclass file: 04_OVM_mechanics/07_toy_factory/top.sv 59 class wrapper_base; 60 virtual function object_base create_object(); 61 return null; 62 endfunction 63 endclass file: 04_OVM_mechanics/07_toy_factory/top.sv
object_base имеет виртуальную функцию print(), которую мы используем для проверки типов, созданных на фабрике. wrapper_base имеет виртуальную функцию create(), полиморфный конструктор, который используется для создания новых объектов. factory - это singleton, то есть его конструктор локальный, и он содержит статическую ссылку на самого себя. Единственный способ создать экземпляр factory является вызов factory::get(). factory содержит ассоциативный массив, который отображает запрашиваемые типы wrapper_base в переопределенные типы wrapper_base.
73 class factory; 74 75 static factory f; 76 wrapper_base override_map[wrapper_base]; 77 78 local function new(); 79 endfunction 80 81 static function factory get(); 82 if(f == null) 83 f = new(); 84 return f; 85 endfunction 86 87 function void register(wrapper_base w); 88 override_map[w] = w; 89 endfunction 90 91 function void set_override(wrapper_base requested_type, 92 wrapper_base override_type); 93 override_map[requested_type] = override_type; 94 endfunction 95 96 function object_base create(wrapper_base 97 requested_type); 98 object_base obj; 99 wrapper_base override_type = 100 override_map[requested_type]; 101 obj = override_type.create_object(); 102 return obj; 103 endfunction 104 105 endclass file: 04_OVM_mechanics/07_toy_factory/top.sv
Метод register() добавляет новую запись в карту. Первоначально, при регистрации, тип не имеет переопределения. Таким образом, мы записали в карту тип, указывающий сам на себя. Set_override () метод заменяет запись в карте новым переопределенным типом. create() метод ищет переопределения на указанный тип, инициирует создание переопределенного типа и возвращает вновь созданный объект.
Класс-оболочка является наиболее интересным классом в нашем созвездии factory-related классов. Хотя он довольно прост, он делает большую часть тяжелой работы. Это основной интерфейс для фабрики, и большинство операций, которые производятся фабрикой, проходят через интерфейс оболочки.
118 class wrapper #(type T=object_base) extends wrapper_base; 119 120 typedef wrapper#(T) this_type; 121 122 static this_type type_handle = get_type(); 123 124 local function new(); 125 endfunction 126 127 function object_base create_object(); 128 T t = new(); 129 return t; 130 endfunction 131 132 static function T create(); 133 T obj; 134 factory f = factory::get(); 135 assert($cast(obj, f.create(get_type()))); 136 return obj; 137 endfunction 138 139 static function this_type get_type(); 140 factory f; 141 if(type_handle == null) begin 142 type_handle = new(); 143 f = factory::get(); 144 f.register(type_handle); 145 end 146 return type_handle; 147 endfunction 148 149 static function void set_override(wrapper_base 150 override_type); 151 factory f = factory::get(); 152 f.set_override(type_handle, override_type); 153 endfunction 154 155 endclass file: 04_OVM_mechanics/07_toy_factory/top.sv
Все функции в wrapper#() являются статическими, за исключением конструктора и create_object (). Мы можем выполнить его статические функции, не заботясь о том, был ли они явно создан экземпляр класса. Так как каждая обертка специализации уникальна. Это означает, что статическая переменная type_handle является уникальной и может быть использован в качестве прокси-сервера для обернутого типа (то есть, типа, передающегося в качестве параметра, который используется для специализации класса). Так как тип является уникальным, и большинство методов являются статическими, мы можем рассматривать типа больше как тип, а не объект.
Тип инициализируется статически. Это происходит в следующей строке:
static wrapper#(T) type_handle = get_type();
Функция get_type () вызывается во время статической инициализации, которая не только создает экземпляр оболочки, но и регистрирует его в фабрике. Для регистрации класса в фабрике, сначала специализируется обертка с типом объекта, который вы оборачиваете. Используйте typedef для специализации оболочки, как показано в следующем примере:
typedef wrapper#(some_type) type_id;
Это определение типа создает оболочку для SOME_TYPE типа, тип, производный от object_base.
Рисунок 4-8 иллюстрирует, как использовать фабрику с некоторыми классами A, B, и С, которые являются производными от family_base.
Чтобы зарегистрировать классы с помощью фабрики, каждый из них имеет typedef из обертки, параметризованной со своим типом. Ниже класс А. Класс B и C похожи. Каждый из них имеет typedef, которая специализируется обертку.
169 class A extends family_base; 170 171 typedef wrapper#(A) type_id; 172 173 virtual function void print(); 174 $display(“A”); 175 endfunction 176 endclass file: 04_OVM_mechanics/07_toy_factory/top.sv
206 function void run(); 207 208 f = factory::get(); 209 210 h = family_base::type_id::create(); 211 h.print(); 212 213 family_base::type_id::set_override(B::type_id::get_type()); 214 215 h = family_base::type_id::create(); 216 h.print(); 217 218 endfunction file: 04_OVM_mechanics/07_toy_factory/top.sv
The code makes heavy use of the double-colon (::) scope operator. Он используется для обращения к статической функции в factory и в wrapper#(). Сначала, мы получаем уникальный экземпляр структуры данных фабрики, затем создаем экземпляр объекта family_base. family_base :: type_id :: get_type() является статической функции внутри оболочки специализации для family_base. Мы проверяем, что экземпляр family_base создается путем вызова print(). Далее, определяем переопределение B для family_base. Опять же, мы создаем экземпляр family_base. На этот раз, так как переопределение сейчас находится на месте, вместо того, чтобы получить экземпляр family_base, мы получаем экземпляр B.
Наша фабрика не содержит все функциональные возможности фабрики, реализованной в OVM. Фабрика OVM обеспечивает отображение строк (имен) в типы. Это позволяет переопределить цепочки, в то время, как фабрика этого не делает. Например, если B переопределяет А и C переопределяет B, когда вы просите экземпляр А, вы получите экземпляр C. Фабрика OVM поддерживает два начальных базовых класса, ovm_object и ovm_component для зарегистрированных объектов, и метод create() для обоих, в то время, фабрика имеет только один первичный класс, object_base.