Приветствую тебя, дорогой единомышленник! В данном репозитории содержатся исходные файлы статей Telegram-канала Verification For All.
Список статей:
- SystemVerilog и виртуальный интерфейс.
- Верификация на SystemVerilog. "Гонки" сигналов на симуляции.
- Кодовое покрытие в функциональной верификации.
- Обзор SystemVerilog IEEE 1800-2023.
- Demystifying UVM: Фабрика, часть 1.
- Demystifying UVM: Фабрика, часть 2.
- SystemVerilog Gotchas, Tips and Tricks, часть 1.
- SystemVerilog Gotchas, Tips and Tricks, часть 2.
SystemVerilog и виртуальный интерфейс. Связь статического и динамического мира симуляции.
Вместе с большим количеством нововведений несинтезируемого подмножества (class, fork-join, randomize(), queue, ...) SystemVerilog "порадовал" инженеров новыми синтезируемыми конструкциями. Одной из них являлся интерфейс (interface).
Задумка здесь очень простая. Интерфейс по своей сути является сгруппированным набором сигналов. Например, для простейшего AXI-Stream этот набор может быть объявлен следующим образом:
interface axi_s_intf (input logic clk);
// Simple AXI-Stream interface
// TSTRB, TKEEP, TID, TDEST, TUSER
// are not used for this impl.
logic tvalid;
logic tready;
logic [31:0] tdata;
logic tlast;
endinterface
Интерфейс обладает статической природой (как, например, и модуль). Его экземпляр создается в нулевой момент времени симуляции и не может быть удален до ее завершения.
module testbench;
// Тактовый сигнал и сигнал сброса
logic clk;
logic aresetn;
// Экземпляры AXI-Stream интерфейса
axi_s_intf intf_1 (clk);
axi_s_intf intf_2 (clk);
// Дизайн для верификации
my_design DUT (
.clk ( clk ),
.aresetn ( aresetn ),
// AXI-Stream 1
.tvalid_1 ( intf_1.tvalid ),
.tready_1 ( intf_1.tready ),
.tdata_1 ( intf_1.tdata ),
// AXI-Stream 2
.tvalid_2 ( intf_2.tvalid ),
.tready_2 ( intf_2.tready ),
.tdata_2 ( intf_2.tdata ),
.tlast_2 ( intf_2.tlast )
);
Как правило, экземпляры интерфейсов объявляются в главном модуле симуляции и подключаются к интерфейсам или портам тестируемого устройства.
Заметим, что не все провода интерфейса обязательно должны быть использованы при его подключении. Например, в примере выше провод tlast для интерфейса intf_1 не подключается.
Структурно и в системной памяти имеем следующую картину:
Статические элементы в начале симуляции занимают место в памяти и существуют до завершения симуляции.
Так как в SystemVerilog появились динамические объекты (class), то возникла также необходимость в получении доступа из динамических объектов к статическим элементам верификационного окружения. Для этих целей и существует виртуальный интерфейс.
Виртуальный интерфейс - указатель на статический экземпляр интерфейса. В ходе симуляции виртуальный интерфейс может указывать на различные экземпляры.
module testbench;
// Тактовый сигнал и сигнал сброса
logic clk;
logic aresetn;
// Экземпляры AXI-Stream интерфейса
axi_s_intf intf_1 (clk);
axi_s_intf intf_2 (clk);
...
// Виртуальный интерфейс AXI-Stream
virtual axi_s_intf vif;
initial begin
$display(vif);
vif = intf_1;
$display(vif);
end
endmodule
Результатом запуска симуляции в таком случае будет:
# null
# /testbench/intf_1
В начале виртуальный интерфейс не указывает ни на какой из статических интерфейсов, то есть проинициализирован null(нулевым указателем). Строка vif = intf_1 определяет для виртуального интерфейса статический экземпляр интерфейса, на который он будет указывать.
При помощи виртуального интерфейса пользователь может взаимодействовать с сигналами статического интерфейса. В ходе симуляции виртуальный интерфейс может использоваться для изменения сигналов различных статических интерфейсов.
module testbench;
// Тактовый сигнал и сигнал сброса
logic clk;
logic aresetn;
// Экземпляры AXI-Stream интерфейса
axi_s_intf intf_1 (clk);
axi_s_intf intf_2 (clk);
initial begin
clk <= 0;
forever begin
#5 clk <= ~clk;
end
end
...
// Виртуальный интерфейс AXI-Stream
virtual axi_s_intf vif;
initial begin
vif = intf_1;
$display(vif);
@(posedge clk);
vif.tvalid <= 1;
vif.tdata <= 10;
@(posedge clk);
vif = null;
@(posedge clk);
vif = intf_2;
$display (vif);
@(posedge clk);
vif.tvalid <= 1;
vif.tdata <= 10;
vif.tlast <= 1;
@(posedge clk);
vif = null;
end
endmodule
Результаты симуляции:
# /testbench/intf_1
# /testbench/intf_2
Заметим, что в промежутке между присвоениями указателей на статические интерфейсы виртуальный "успел побывать" и в нулевом (null) значении. В ходе симуляции указатель, содержащийся в виртуальном интерфейсе, может динамически создаваться и уничтожаться.
Изображение ниже демонстрирует процесс создания и удаления указателя на статические интерфейсы (пример кода выше). Изначально виртуальный интерфейс указывал на intf_1 (отмечено -->), после чего был проинициализирован null, а после стал указывать на intf_2 (отмечено ->).
Виртуальные интерфейсы в большинстве случаев используются для передачи в объекты классов, в которых определен набор задач для манипулирования сигналами интерфейса.
module testbench;
// Тактовый сигнал и сигнал сброса
logic clk;
logic aresetn;
// Экземпляры AXI-Stream интерфейса
axi_s_intf intf_1 (clk);
axi_s_intf intf_2 (clk);
...
class my_design_driver;
virtual axi_s_intf vif;
function new(virtual axi_s_intf vif);
this.vif = vif;
endfunction
task drive();
@(posedge vif.clk);
vif.tvalid <= 1;
vif.tdata <= $random();
@(posedge vif.clk);
vif.tvalid <= 0;
endtask
endclass
my_design_driver driver;
initial begin
driver = new(intf_1);
repeat(10) driver.drive();
end
endmodule
Результат симуляции кода:
В данном примере в конструктор класса my_design_driver передается статический интерфейс. Однако в аргументе конструктора тип аргумента объявлен как virtual axi_s_intf, то есть происходит присвоение virtual axi_s_intf vif = axi_s_intf intf_1, что является абсолютно легальным в SystemVerilog (было разобрано в примерах выше).
Как видите, ничего сложного! Больше заметок вы можете найти в Telegram-канале автора Verification For All.
Хорошего тебе дня, читатель, и до новых встреч!
Верификация на SystemVerilog. "Я же все правильно написал, почему не работает?" или "гонки" сигналов на симуляции.
Вступление
Приветствую тебя, читатель!
Проверить 8-битный последовательностный сумматор. Казалось бы, что может быть проще? Но есть нюансы.
Входные данные
Итак, имеем дизайн:
module sum (
input logic clk,
input logic [7:0] a,
input logic [7:0] b,
output logic [7:0] c
);
always_ff @( posedge clk) begin
c <= a + b;
end
endmodule
Напишем простейшее верификационное окружение.
Пишем верификационное окружение
Создадим нужные сигналы и подключим модуль.
logic clk;
logic [7:0] a;
logic [7:0] b;
logic [7:0] c;
sum DUT (
.clk(clk),
.a (a ),
.b (b ),
.c (c )
);
Сгенерируем тактовый сигнал. forever - бесконечный цикл.
initial begin
clk <= 0;
forever #10 clk <= ~clk;
end
Подадим входные воздействия. 10 раз (repeat(10)) значение в интервале от 0 до 5 ($urandom_range(0,5)).
initial begin
repeat(10) begin
@(posedge clk);
a = $urandom_range(0, 5);
b = $urandom_range(0, 5);
end
$stop();
end
Реализуем логирование данных. Создаем mailbox, куда каждый такт отправляем данные с входных и выходных портов в виде структуры packet.
typedef struct {
logic [7:0] a;
logic [7:0] b;
logic [7:0] c;
} packet;
mailbox#(packet) mbx = new();
packet p;
initial begin
forever begin
@(posedge clk);
p.a = a;
p.b = b;
p.c = c;
mbx.put(p);
end
end
Осталось циклически проводить проверку каждый такт. Забираем их mailbox пакеты и сравниваем, что результат текущего такта (c) равен сумме операндов прошлого такта (a и b).
initial begin
mbx.get(p1);
forever begin
mbx.get(p2);
if( p2.c !== p1.a + p1.b ) begin
$error("%t Real: %h, Expected: %h",
$time(), p2.c, p1.a + p1.b);
end
p1 = p2;
end
end
Полный код окружения размещен в файле testbench.sv.
Запускаем
Симулятор, используемый в примерах: QuestaSim.
Получается, все? Запускаем симуляцию!
cd src/
vlog *.sv
vsim -gui testbench -voptargs="+acc"
Неожиданно сталкиваемся с ошибками. Проблема в дизайне? Не думаю.
run -all
# ** Error: 50 Real: 07, Expected: 04
# Time: 50 ns Scope: testbench File: testbench.sv Line: 55
# ** Error: 90 Real: 01, Expected: 04
# Time: 90 ns Scope: testbench File: testbench.sv Line: 55
# ** Error: 170 Real: 07, Expected: 03
# Time: 170 ns Scope: testbench File: testbench.sv Line: 55
# ** Note: $stop : testbench.sv(26)
# Time: 190 ns Iteration: 1 Instance: /testbench
Смотрим временную диаграмму. Перемещаемся в момент времени 50ns, потому что согласно логу выше первая ошибка была обнаружена именно в этот момент времени.

Хм, кажется, все верно, 0x3 + 0x4 = 0x7. Ошибка не наблюдается. В чем же проблема? И ведь тестбенч показывает, что результат должен быть 0x4. Как будто это уже результат для следующего такта.
Проблема здесь кроется в блокирующих присваиваниях (=) вместо неблокируюищих (<=) в коде генерации входных воздействий. Почему это важно? Следите за руками.
SystemVerilog и регионы выполнения
Каждый уважающий себе верификатор знает, что выполнение событий симуляции распределено по так называемым "регионам выполнения" или "регионам событий" (Event Region). Попадая в конкретный момент времени, симулятор обрабатывает события в некоторой последовательности, определенной стандартом SystemVerilog.
Обратим внимание на два региона: Active и NBA.
Применительно к присваиваниям: все блокирующие (=) происходят в случайном порядке в регионе Active, все неблокирующие (<=) тоже в случайном порядке в NBA.
Когда я говорю "в случайном порядке", я имею в виду порядок относительно независимых процессов. То есть, если у вас есть два initial-блока, которые выполняются совместно:
initial begin
a = 10;
a = 20;
end
initial begin
b = 5
b = 7;
end
То, присвоения внутри begin-end происходят последовательно, то есть после выполнения a будет таки равно 20, а b равно 7. Однако симулятор может выполнять присвоения из этих двух блоков в любой последовательности.
Например:
a = 10;b = 5;b = 7;a = 20;
Или:
b = 5;a = 10;a = 20;b = 7;
Подробный разбор регионов выполнения можно найти тут и тут. Рекомендую посмотреть перед тем, как продолжим.
Также по этой теме рекомендую статью SystemVerilog Event Regions, Race Avoidance & Guidelines от Clifford Cummings.
Вооружившись знаниями, вернемся к симуляции.
Посыпаем голову пеплом
Мы имеем интересную ситуацию. Совместно у нас исполняются initial-блоки генерации входных воздействий и мониторинга.
initial begin
repeat(10) begin
@(posedge clk);
a = $urandom_range(0, 5);
b = $urandom_range(0, 5);
end
$stop();
end
...
initial begin
forever begin
@(posedge clk);
p.a = a;
p.b = b;
p.c = c;
mbx.put(p);
end
end
Это означает, что в данном случае после каждого фронта (@(posedge clk)) последовательность выполнения не определена.
Она может быть такой (что нас устраивает):
p.a = a;p.b = b;p.c = c;mbx.put(p);a = $urandom_range(0, 5);b = $urandom_range(0, 5);
А может быть и такой (что нас не устраивает):
a = $urandom_range(0, 5);b = $urandom_range(0, 5);p.a = a;p.b = b;p.c = c;mbx.put(p);
Вернемся к моменту времени в 50ns.

А теперь переместимся в момент предыдущего такта (`30ns``) и переключимся в режим Events Mode (правой кнопкой мыши по временной диаграмме -> Expanded Time -> Events Mode, затем снова правой кнопкой мыли по временной диаграмме -> Expanded Time -> Expand All).

Приблизим момент времени 30ns.

Видим значения 30ns + 2 + 4 + .... Это как раз таки очередность изменения сигналов в ходе одного региона выполнения времени 30ns. А теперь внимательно посмотрите, какие данные сохраняются в пакет p. Верно, 0x0 и 0x4. Почему? Потому что сначала выполнилось a = $urandom_range(0,5) (вернуло 0), а затем p.a = a. Получается, что вместо 0x3 и 0x4 в пакет попали 0x0 и 0x4.
a = $urandom_range(0, 5);p.a = a;p.b = b;p.c = c;mbx.put(p);
Для b = $urandom_range(0, 5) точно сказать нельзя, потому что случайное число этого такта совпало с числом на предыдущем такте и наблюдать изменения мы не можем. Но в данном контексте это не важно.
А что происходит дальше? Передвигаемся в следующий такт и попадаем в момент 50ns.

Здесь нам важно только сохранение результата. Обратите внимание, что сохранение результата 0x7 происходит до его обновления по фронту, т.к. значение c обновляется в коде сумматора через <=.
После получения пакета с результатом в этом же такте происходит происходит сравнение: 0x0 + 0x4 !== 0x7 (данные из пакета p "попадают" пакет p2 сравниваются с данными из p1). Откуда 0x0 и 0x4 в p1? Так с предыдущего такта, где вместо 0x3 в пакет попало значение 0x0.
Явление, описанное выше, когда неопределенность последовательности выполнения событий симулятором приводит к неопределенному поведению симуляции, и называется "гонками сигналов" (англ. race condition).
Исправляем ошибки
Так, в чем проблема - разобрались. Остался вопрос: как её решить? На самом деле очень просто. Нужно помнить одно важное правило: при взаимодействии с портами тестируемого последовательностного устройства используется неблокирующее присваивание (<=).
initial begin
repeat(10) begin
@(posedge clk);
a <= $urandom_range(0, 5);
b <= $urandom_range(0, 5);
end
$stop();
end
Как это поможет? Так ведь значения, присвоение которым происходит через неблокирующее присваивание (<=), в обязательном порядке выполняются после выполнения всех блокирующих (вспомните раздел SystemVerilog и регионы выполнения). Для нас это значит, что a <= $urandom_range(0 ,5) и b <= $urandom_range(0, 5) выполнятся после сохранения информации о входных значениях, выставленных на предыдущем такте.
Сохраняем изменения и запускаем симуляцию. Ошибки пропали.
run -all
# ** Note: $stop : testbench.sv(26)
# Time: 190 ns Iteration: 1 Instance: /testbench
Код исправленного окружения размещен в файле testbench_fixed.sv.
Давайте сравним время 30ns для ошибочного и справленного модулей тестирования. Сверху ошибочный модуль, снизу - исправленный.

Видим, что в исправленном окружении значение 0x0 подается на вход a после сохранения информации о значениях на входах в текущий момент времени. Происходит это потому, что выставление значения на вход делается через неблокирующее присваивание (<=), а считывание значений через блокирующее.
Здесь мы сами для себя дополнили озвученное выше правило, звучать оно теперь будет так:
При тестировании последовательностного устройства входные воздействия следует подавать через неблокирующие (<=) присваивания, а считывать выходные и проверять их - через блокирующие (=).
Таким образом, в моменте времени 30ns получаем пакет с верными данными 0x3 и 0x4. Далее, в моменте времени 50ns (на следующем такте) получаем данные с выхода.

После получения пакета с результатом в этом же такте происходит происходит сравнение: 0x3 + 0x4 !== 0x7 (данные из пакета p "попадают" пакет p2 сравниваются с данными из p1). Откуда 0x3 и 0x4 в p1? Так с предыдущего такта, где они были сохранены в процессе мониторинга.
Заключение
Что ж, читатель, мы с тобой детально разобрали такое непростое, но одновременно интересное явление, как "гонки сигналов" на симуляции. В ходе разбора вывели правило, которое обезопасит меня, тебя и еще множество инженеров от потраченного на поиск ошибки времени и нервов.
А больше подобных заметок ты можешь найти в Telegram-канале автора Verification For All.
Хорошего тебе дня и до новых встреч!
Кодовое покрытие в функциональной верификации: все говорят, но никто не использует.
Вступление
Приветствую тебя, дорогой читатель! Данная заметка посвящена кодовому покрытию (англ. code coverage). Это одна из метрик, определяющая полноту проверки устройства. Кодовое покрытие определяет, сколько раз и какие именно участки кода были выполнены в ходе симуляции работы устройства.
Так уж сложилось, что начинающими инженерами кодовое покрытие собирается и анализируется реже, чем то же функциональное (исходя из личного опыта автора). Хотя имеет свои неоспоримые преимущества и причины, по которым этот тип покрытия должен учитываться верификатором.
Motivation или зачем это всё
В данной заметке я бы хотел познакомить тебя с кодовым покрытием, привести простые примеры его сбора и анализа. А еще я бы хотел показать, насколько полезным может быть включение сбора кодового покрытия в верификационный план.
Повествование будет построено по принципу "от простого к сложному", и, в основном, на примерах. Все рассуждения и пояснения касаются исключительно Verilog и SystemVerilog. VHDL не трогаем. Для симуляции работы цифровых устройств будем использовать QuestaSim 10.7c. Запускаться будем под Linux CentOS 7. Поехали!
P.S. Сопроводительные материалы (в основном код примеров и команды для запуска) теперь будут публиковаться в GitHub в репозитории. На момент публикации этой заметки, все сопроводительные материалы для нее уже загружены в репозиторий.
Начинаем с малого
Чтобы сразу быть к контексте, разберем простейший пример. Сначала рассмотрим дизайн, который будем проверять, потом тестовую среду для него, потом запустим симуляцию со сбором кодового покрытия и проанализируем результаты.
Подопытный
Конвертер двоичного кода (вход binary) в one-hot (выход onehot):
module binary_to_onehot (
input logic [1:0] binary,
output logic [3:0] onehot
);
always_comb begin
case(binary)
2'b00: onehot = 4'b0001;
2'b01: onehot = 4'b0010;
2'b10: onehot = 4'b0100;
2'b11: onehot = 4'b1000;
endcase
end
endmodule
Тестирование
Напишем простой тестовый сценарий. Просто будем подавать на входы значения.
module testbench;
logic [1:0] binary;
logic [3:0] onehot;
binary_to_onehot DUT (
.binary ( binary ),
.onehot ( onehot )
);
initial begin
binary = 2'b11;
#10;
binary = 2'b01;
#10;
$finish();
end
endmodule
Запуск
Компилируем исходники и запускаем симуляцию:
vlog *.sv
vsim testbench -coverage -voptargs="+cover=s+/testbench/DUT" -do "run -a;"
Передав необходимые опции в команду симуляции, мы активировали сбор одного из типов кодового покрытия - процедурного покрытия (англ. statement coverage).
Если далее в QuestaSim выбрать View -> Coverage -> Code Coverage Analysis, то откроется окно, в котором можно проанализировать кодовое покрытие.
Если выберем DUT в иерархии, то во вкладке Code Coverage Analysis увидим:
При сборе такого типа кодового покрытия, ПО в ходе симуляции анализирует выполнение кода, после чего формирует отчет о том, какие процедурные выражения были выполнены. На изображении выше видим пример такого отчета. Из всех возможных выражений выполнилось 3. Почему? Давай разбираться.
Анализ результатов
Так, ну список возможных выражений вполне понятен. Модуль состоит из одного always_comb блока. Внутри него case, в зависимости от входного значения, формирует выходное.
// ...
always_comb begin
case(binary)
2'b00: onehot = 4'b0001;
2'b01: onehot = 4'b0010;
2'b10: onehot = 4'b0100;
2'b11: onehot = 4'b1000;
endcase
end
// ...
Блок always_comb выполняется при каждом изменении переменной внутри этого блока. Очевидно, что если вход binary изменился хоть раз в ходе симуляции, то и блок должен выполниться. Видим это в отчете (см. выше) в виде зеленой галки напротив соответствующего выражения.
Присвоение определенного значения выхода onehot выполняется в зависимости от значения на входе binary. 2'b00 соответствует 4'b0001, 2'b01 соответствует 4'b0010 и так далее. У нас выполнилось: 2'b01: onehot = 4'b0010;, 2'b11: onehot = 4'b1000;. Ну так понятно, почему! Мы ведь в нашем тестбенче (см. выше) подали значения : 2'b01, 2'b11:
// ...
initial begin
binary = 2'b11;
#10;
binary = 2'b01;
#10;
$finish();
end
// ...
которые и привели к выполнению соответствующих участков кода.
А если посложнее
Так, ну с базовым примером разобрались, теперь сделаем модуль для тестирования чуть интереснее. Алгоритм тот же, разбираем дизайн, тесты, запускаемся и анализируем результаты.
Подопытный
Можем считать, что это некий декодер, который в зависимости от входа s определяет некоторые значения выходов a, b и c.
module decoder (
input logic [1:0] s,
output logic a,
output logic b,
output logic c
);
always_comb begin
if( s[0] ) begin
a = 0;
b = 0;
c = 0;
end
else begin
case( s[1] )
1'b0: begin
a = 0;
b = 1;
c = 1;
end
1'b1: begin
a = 1;
b = 1;
c = 1;
end
endcase
end
end
endmodule
Тестирование
Тут все тоже тривиально. Подаем на входы значения.
module testbench;
logic [1:0] s;
logic a;
logic b;
logic c;
decoder DUT (
.s ( s ),
.a ( a ),
.b ( b ),
.c ( c )
);
initial begin
s = 2'b00;
#10;
s = 2'b10;
#10;
$finish();
end
endmodule
Запуск
Компилируем исходники и запускаем симуляцию:
vlog *.sv
vsim testbench -coverage -voptargs="+cover=sb+/testbench/DUT" -do "run -a;"
Передав необходимые опции, мы активировали сбор уже двух типов кодового покрытия - процедурного покрытия (англ. statement coverage) и покрытия ветвления (англ. branch_coverage).
Во вкладке Code Coverage Analysis увидим:
Хм, в целом картина похожая. Часть выражений выполнена, а часть нет. Добавилось ли что-то новое? Да! Я хочу, чтобы ты обратил внимание на верхний правый угол. Там, где написано Statement. Это как раз кнопка выбора типа кодового покрытия. Если нажмем на нее, то сможем выбрать Branch, то есть покрытие ветвления.
Анализ результатов
Вообще, если посмотреть на оба отчета, то в нашем случае они говорят об одном и том же, просто разным "языком".
Давайте вернемся к коду тестируемого модуля.
// ...
if( s[0] ) begin
a = 0;
b = 0;
c = 0;
end
else begin
case( s[1] )
1'b0: begin
a = 0;
b = 1;
c = 1;
end
1'b1: begin
a = 1;
b = 1;
c = 1;
end
endcase
end
// ...
Что нам говорит отчет о покрытии ветвлений? Что переход else-begin, а также все переходы внутри case были выполнены. Если посмотреть код тестового сценария, то очевидно, почему. А если посмотреть на процедурный отчет, то он как раз говорит нам о том, что выполнились выражения, которые и должны были выполниться при обозначенных переходах (ну и always_comb в придачу).
А не выполнился только лишь переход if-begin, потому что в тестовом сценарии s[0] никогда не принимало значение 1. А что нам говорит процедурное покрытие? Верно, не выполнились выражения, которые должны были бы выполниться при переходе if-begin.
Немного теории
Типы кодового покрытия
Так, разбирая примеры, мы с вами попутно познакомились с двумя часто используемыми типами кодового покрытия: процедурным покрытием (англ. statement coverage) и покрытием ветвлений (англ. branch coverage).
Процедурное покрытие предназначено для сбора информации о том, какие независимые участки кода выполнились в ходе симуляции. Сюда входит выполнение блоков always_ff, always_comb, процедуры assign, просто единичных присвоений = и так далее.
Покрытие ветвлений предназначено для сбора информации о том, какие условные переходы были произведены в ходе симуляции. Сюда входит анализ конструкций if-else, case, тернарных операторов.
Существуют и другие типы, но об этом позже.
Замечу, что для примеров я выбрал два обозначенных выше типа кодового покрытия, так как они являются "минимальной базой", без которой практически пропадает смысл всего затеянного. А теперь, читатель, нам нужно разобраться в особенностях кодового покрытия и его отличиях от функционального.
Сбор функционального покрытия
Для сбора функционального покрытия используются специальные конструкции SystemVerilog (covergroup, coverpoint, bin и т.д.). То есть, для сбора функционального покрытия, верификатор сам пишет необходимый код, который и определяет ту область, которую он хочет отслеживать.
Например, если тестируется модуль конвертера:
module testbench;
logic [1:0] binary;
logic [3:0] onehot;
binary_to_onehot DUT (
.binary ( binary ),
.onehot ( onehot )
);
initial begin
binary = 2'b11;
#10;
binary = 2'b01;
#10;
$finish();
end
endmodule
То для покрытия значений входа binary нужно внутри модуля testbench написать:
// ...
covergroup dut_cg @(binary);
coverpoint binary;
endgroup
dut_cg cg = new();
// ...
После запуска тестового сценария можно будет наблюдать:
То есть в ходе симуляции вход binary принимал 2 из 4 возможных значений. При этом мы сами описали, что хотим отслеживать именно его. Здесь еще раз замечу, что для сбора функционального покрытия, верификатор должен обладать знаниями соответствующего синтаксиса SystemVerilog.
Сбор кодового покрытия
Для получения кодового покрытия инженеру не нужно ничего описывать при помощи синтаксиса SystemVerilog. Все необходимое уже имеется "под капотом" у симулятора. Верификатор просто активирует сбор при помощи определенных опций (флагов, передаваемых в команды запуска) и анализирует полученный результат.
Рассмотрим команду запуска симуляции из примера выше:
vsim testbench -coverage -voptargs="+cover=sb+/testbench/DUT" -do "run -a;"
Флаг -coverage отвечает за включение сбора кодового покрытия. Флаг -voptargs="+cover=sb+/testbench/DUT" отвечает за активацию процедурного покрытия (+vcover=s) и покрытия условий (+vcover=b) для экземпляра модуля с именем DUT (+/testbench/DUT).
То есть, для настройки функционального покрытия используются конструкции SystemVerilog, а для настройки кодового покрытия используются флаги и опции симулятора.
Заметим, что флаги кодового покрытия специфичны для конкретного ПО. А это наталкивает нас на мысль о том, что на самом деле кодовое покрытие зависит от используемого ПО (англ. tool specific), потому что аргументы для его сбора определяются симулятором, который используется.
Пример аргументов кодового покрытия для QuestaSim:
-voptargs="+cover=bcesxf"
Пример аргументов кодового покрытия для VCS:
-cm line+cond+fsm+tgl+branch+assert
На самом деле, отличаются не только аргументы, но и формат представления. Также могут варьироваться и типы кодового покрытия. Об этом в разделах ниже.
Специфичность кодового покрытия
Сбор кодового покрытия не ограничен никаким стандартом и специфичен для симулятора, а это значит, что здесь открывается простор для реализации различных типов, а также дополнительных опций.
Например, в QuestaSim, кроме процедурного покрытия и покрытия ветвлений, присутствуют следующие типы:
- покрытие условий (англ. condition coverage) - для отслеживания состояний логических выражений, определяющих условные переходы;
- покрытие выражений (англ. expression coverage) - для отслеживания состояний логических выражений;
- покрытие переключений (англ. toggle coverage) - для отслеживания изменения значения каждого бита переменных;
- покрытие состояний конечных автоматов (англ. FSM coverage) - для отслеживания состояний конечных автоматов;
- покрытие классов (англ. class coverage) - для отслеживания типов созданных объектов классов.
В качестве дополнительных опций можно выделить гибкую настройку элементов симулируемой системы, на которые будет распространяться сбор кодового покрытия (например +/testbench/DUT), а также сбор покрытия переключений только для портов (-toggleportsonly). Очевидно, что это лишь примеры опций, в реальности их гораздо больше.
Взаимозаменяемость кодового покрытия
Набор типов кодового покрытия может варьироваться от симулятора к симулятору, однако для "большой тройки" это характерно в меньшей степени. Например, в VCS после QuestaSim я не нашел только покрытие классов. Для остальных типов же есть соответствие:
- statement coverage (Questa) - line coverage (VCS)
- branch coverage (Questa) - branch coverage (VCS)
- condition coverage, expression coverage (Questa)- conditional coverage (VCS)
- toggle coverage (Questa) - toggle coverage (VCS)
- FSM coverage (Questa) - FSM coverage (VCS)
Стоит заметить, что названия типов, конечно, не повторяются "слово в слово", а также одиночный тип одного симулятора может инкапсулировать в себе несколько типов другого. Так что, в любом случае, придется потратить некоторое время для сопоставления типов.
А вот для опций это менее характерно, здесь, "есть, где разгуляться", и у каждого симулятора они свои, хотя, конечно, есть и повторяющиеся.
Переносимость кодового покрытия
Что если нужно "мигрировать" с одного симулятора на другой? Как обстоят дела с переносом кодового покрытия?
Для функционального покрытия в этом плане все просто: написал код на SystemVerilog, а значит он будет работать в любом симуляторе, который поддерживает необходимое подмножество языка. Сбор будет осуществляться посредством выполнения команд этого подмножества.
Для кодового покрытия есть нюансы. Так как флаги сбора, опции и типы варьируются от симулятора к симулятору, то пользователю в любом случае придется открыть соответствующую документацию (чаще всего это User Guide и Command Reference Manual) для воспроизведения конфигурации, которая использовалась в другом симуляторе.
Сохранение кодового покрытия
В режиме GUI в ходе симуляции и после ее завершения, пользователь может просмотреть отчет о кодовом покрытии. Вырезки из таких отчетов и были представлены при разборе примеров. Но что если инженер хочет сохранить результаты "до лучших времен". Конечно, такая возможность присутствует. Причем формат может быть разным даже в рамках одного симулятора.
Вернемся к тестированию декодера:
module decoder (
input logic [1:0] s,
output logic a,
output logic b,
output logic c
);
always_comb begin
if( s[0] ) begin
a = 0;
b = 0;
c = 0;
end
else begin
case( s[1] )
1'b0: begin
a = 0;
b = 1;
c = 1;
end
1'b1: begin
a = 1;
b = 1;
c = 1;
end
endcase
end
end
endmodule
После завершения симуляции в режиме GUI можем наблюдать отчет:
Однако, если закрыть GUI, то результаты будут утеряны.
Для сохранения результатов в файл базы данных специального формата .ucdb следует добавить к команде запуска coverage save <имя-файла>.ucdb.
Полная команда запуска:
vsim testbench -coverage -voptargs="+cover=sb+/testbench/DUT" -do "run -a; coverage save cov.ucdb;"
Сохраненную базу данных сможем в любое время открыть командой:
vsim -viewcov cov.ucdb
Но и это еще не все. Также мы можем сформировать детализированный текстовый отчет или HTML отчет, для просмотра которого не нужен будет сам симулятор. Для этого следует добавить к команде запуска coverage report -details -file <имя-файла>.txt для текстового формата и coverage report -html -details -htmldir <имя-директории> для HTML формата.
Пример соответствующих команд:
vsim testbench -coverage -voptargs="+cover=sb+/testbench/DUT" -do "run -a; coverage report -details -file cov.txt;
vsim testbench -coverage -voptargs="+cover=sb+/testbench/DUT" -do "run -a; coverage report -html -details -htmldir htmlcov;"
Открыть можем текстовым редактором и браузером:
nano cov.txt
firefox htmlcov/index.html
Пример HTML-отчета (но лично я предпочитаю текстовый):
Влияние сбора кодового покрытия на скорость симуляции
Сами разработчики симуляторов заявляют, что сбор кодового покрытия может существенно замедлять скорость работы. Не удивительно, ведь, параллельно с симуляцией каждого элемента системы, ПО высчитывает количество выполнения определенных участков кода.
В качестве примера, тест декодера на 100 миллионов случайных значений без кодового покрытия занимает 6 секунд:
time vsim -batch testbench -do "run -a;"
real 0m5.942s
user 0m5.045s
sys 0m0.079s
А с кодовым покрытием двух типов 14.5 секунд:
time vsim -batch testbench -coverage -voptargs="+cover=bs+/testbench/DUT" -do "run -a;"
real 0m14.514s
user 0m13.284s
sys 0m0.086s
Машина одна и та же.
Так что при сборе кодового покрытия внимательно настраивайте области, для которых хотите его осуществлять, стараясь минимизировать работу симулятора.
Причины использовать кодовое покрытие
Так уж сложилось, что начинающими инженерами кодовое покрытие собирается и анализируется реже, чем функциональное. Однако, у него есть свои преимущества и цели использования.
Основные преимущества:
- практически все за вас делает симулятор, нет необходимости писать дополнительный код на SystemVerilog;
- если есть заготовки команд для конкретного ПО, то интегрировать простейший сбор кодового покрытия - дело нескольких минут.
Основные цели:
- проверка задействования всего кода в ходе тестирования;
- поиск "недостающих" тестовых сценариев;
- поиск dead-code.
На целях остановимся чуть подробнее. Очевидно, когда описывается RTL устройства, то подразумевается, что каждое выражение в коде несет смысловую нагрузку и будет выполнено. Кодовое покрытие помогает в этом убедиться.
Также, кодовое покрытие может помочь в поиске недостающих тестовых воздействий. Если часть условных переходов, выражений и т.п. не была выполнена, или, если конечный автомат был не во всех состояниях, то это наталкивает на мысли о неполноте тестовых сценариев.
Ко всему прочему, чаще всего при рефакторинге, остается код, который не выполнится ни при каком сочетании внутреннего состояния устройства и входных воздействий на него (англ. dead code). Казалось бы ничего страшного, но теоретически этот код может повлиять на результаты синтеза, а также, по прошествии времени, заставит мучаться инженеров в попытках понять, зачем он нужен.
Кодовое покрытие и обнаружение бага
Разберем простой пример, в котором кодовое покрытие поможет нам выявить неполноту тестовых воздействий и обнаружить баг. А заодно познакомимся с третьим типом кодового покрытия - покрытием состояний конечного автомата (англ. FSM coverage).
Подопытный
Конечный автомат со схемой переходов:
По спецификации выходы idle, read, write принимают значения логической единицы в соответствии с текущим состоянием автомата. То есть, если автомат в состоянии IDLE, то idle = 1, если в состоянии READ, то read = 1 и так далее.
Однако же, внесем баг. Приравняем выходу write значение логического нуля на постоянной основе.
Исходный код:
module fsm (
input logic clk,
input logic resetn,
input logic req,
input logic we,
input logic ack,
output logic idle,
output logic read,
output logic write
);
typedef enum logic [1:0] {
IDLE = 2'b00,
READ = 2'b01,
WRITE = 2'b10
} state_e;
state_e nextstate, state_ff;
assign idle = state_ff == IDLE;
assign read = state_ff == READ;
assign write = 0;
always_ff @(posedge clk) begin
if(!resetn) state_ff < = IDLE;
else state_ff < = nextstate;
end
always_comb begin
case(state_ff)
IDLE: begin
if( req ) nextstate = we ? WRITE : READ;
else nextstate = IDLE;
end
READ: begin
nextstate = ack ? IDLE : READ;
end
WRITE: begin
nextstate = ack ? IDLE : WRITE;
end
default: nextstate = IDLE;
endcase
end
endmodule
Тесты
Здесь осознанно не привожу полный код, чтобы не перегружать повествование. Обозначу только, что set_inputs() отвечает за выставление значений на входы, а check_ouputs() за проверку значений на выходах, а wait_clocks() за ожидание заданного количества положительных фронтов тактового сигнала.
module testbench;
// ...
// Reset
initial begin
resetn < = 0;
@(posedge clk);
resetn < = 1;
end
// ...
task check_outputs(logic [2:0] outputs);
if( { idle, read, write } !== outputs )
$error("Real: %3b, Expected: %3b",
{ idle, read, write }, outputs);
endtask
// ...
// Generate
initial begin
// Set initial values
req < = 0;
we < = 0;
ack < = 0;
// Wait for unreset
do wait_clocks(1); while(!resetn);
// Check for idle state
check_outputs(3'b100); // idle: 1, read: 0, write: 0
// Check some transitions
// 1
set_inputs(3'b100); wait_clocks(2); // req: 1, we: 0, ack: 0
check_outputs(3'b010); // idle: 0, read: 1, write: 0
// 2
set_inputs(3'b001); wait_clocks(2); // req: 0, we: 0, ack: 1
check_outputs(3'b100); // idle: 1, read: 0, write: 0
//
$finish();
end
endmodule
Запуск
Компилируем исходники и запускаем симуляцию:
vlog *.sv
vsim testbench -coverage -voptargs="+acc +cover=f+/testbench/DUT" -do "run -a;"
Передав необходимые опции, мы активировали сбор кодового покрытия состояний конечного автомата (англ. FSM coverage).
Симуляция завершается без ошибок:
# Loading sv_std.std
# Loading work.testbench(fast)
# Loading work.fsm(fast)
# run -a
# ** Note: $finish : testbench.sv(73)
# Time: 115 ns Iteration: 1 Instance: /testbench
Анализ результатов
Во вкладке Code Coverage Analysis переключимся на FSM на увидим:
Очевидно, что не были совершены переходы IDLE -> WRITE и WRITE -> IDLE.
Если два раза кликнуть на state_ff, то откроется диаграмма переходов, на ней тоже явно можем увидеть количество переходов между состояниями. Примечательно, что симулятор, проанализировав код, сам сделал выводы о том, какие переходы возможны, а какие нет. Красота!
Исправляемся
Допишем тестовый сценарий для инициации нужных переходов:
// ...
// Generate
initial begin
// ...
// 3
set_inputs(3'b110); wait_clocks(2); // req: 1, we: 1, ack: 0
check_outputs(3'b001); // idle: 0, read: 0, write: 1
// 4
set_inputs(3'b001); wait_clocks(2); // req: 0, we: 0, ack: 1
check_outputs(3'b100); // idle: 1, read: 0, write: 0
//
$finish();
end
// ...
Обнаружили баг:
# Loading sv_std.std
# Loading work.testbench(fast)
# Loading work.fsm(fast)
# run -a
# ** Error: Real: 000, Expected: 001
# Time: 75 ns Scope: testbench.check_outputs File: testbench.sv Line: 46
# ** Note: $finish : testbench.sv(78)
# Time: 95 ns Iteration: 1 Instance: /testbench
Смотрим теперь на покрытие состояний:
Все переходы были совершены.
Исправляем RTL:
assign write = state_ff == WRITE;
Запускаемся снова. Результат - без ошибок:
# Loading sv_std.std
# Loading work.testbench(fast)
# Loading work.fsm(fast)
# run -a
# ** Note: $finish : testbench.sv(78)
# Time: 115 ns Iteration: 1 Instance: /testbench
Мы прекрасны!
Заключение
Что ж, дорогой читатель, вот мы и добрались до конца. Было ли интересно? Лично мне - да. Потому что, "стыдно признаться, грех утаить", а я и сам достаточно давно не собирал кодовое покрытие:) Исправлюсь, обещаю.
Надеюсь, прочитав текст выше, ты открыл для себя что-то новое.
Спасибо тебе за уделённое время. Всего наилучшего и до новых встреч!
А больше подобных заметок ты можешь найти в Telegram-канале автора Verification For All.
SystemVerilog IEEE 1800-2023. Обзор нововведений.
Вступление
Доброго времени суток, дорогие читатели! Не могу пройти мимо значительного события в сфере микроэлектроники, а уж в сфере её верификации и подавно.
28 февраля 2024 года была опубликована новая версия стандарта языка SystemVerilog 2023. Итак, что же стандарт 2023 года принесёт нового в существующий "уклад" инженеров? Давайте разбираться!
Данный пост представляет собой краткий авторский обзор нововведений. Обзор каждого нововведения будет содержать примеры, а также ссылки на соответствующие разделы нового стандарта. Приступаем!
1. Наследование функционального покрытия
В SystemVerilog существует такое понятие, как embedded covergroup. Если переводить дословно, то "встроенная" covergroup. Тип такой covergroup объявляется внутри конкретного типа класса и становится "неразрывно" с ним связанным.
Например:
class base;
enum {red, green, blue} color;
covergroup g1;
option.weight = 1;
color_cp: coverpoint color;
endgroup
function new();
g1 = new;
endfunction
endclass
Такая covergroup по умолчанию имеет доступ ко всем полям класса и должна создаваться в функции new() этого класса.
В новой версии стандарта появилась возможность наследовать embedded covergroup. Наследованная covergroup "получает" доступ ко всем элементам (coverpoint, cross и т.д.) родительской, а также всем опциям.
Наследованная covergroup может переопределять элементы и опции родительской, а также добавлять свои.
Например:
class derived extends base;
bit d;
covergroup extends g1;
option.weight = 1;
color_cp: coverpoint color
{
ignore_bins ignore = {blue};
}
d_cp: coverpoint d;
endgroup
function new();
super.new();
endfunction
endclass
В данном примере наследованная covegroup (от covergroup cg1 из примера выше) переопределяет опцию weight и точку покрытия color_cp базовой, а также добавляет новую точку покрытия d_cp.
Соответствующие разделы стандарта: 19.4.1.
2. Функция map() распакованного массива
В новой версии стандарта появилась новая функция map() поэлеметной работы с распакованным массивом.
Основная суть функции заключается в итерировании по массиву и применении к каждому элементу выражения, заключенного в сопутствующую конструкцию with().
Например:
int A [] = {1,2,3}, B [] = {2,3,5}, C [$];
// Add one to each element of an array
A = A.map() with (item + 1'b1); // {2,3,4}
// Add the elements of 2 arrays
C = A.map(a) with (a + B[a.index]); // {4,6,9}
В данном примере при помощи функции map() к каждому элементу массива A сначала прибавляется 1, а после в очередь C записывается результат поэлеметного сложения массивов A и B. Заметим, что функция map() предоставляет пользователю ключевое слово item для доступа к элементу массива и метод index для доступа к его положению в массиве.
Также обратим внимание на важную особенность добавленной функции. Она может возвращать значение типа, отличающегося от типа обрабатываемого массива.
Например:
int A [] = {2,3,4}, B [] = {2,3,5};
// Element by element comparison
bit C [];
C = A.map(a) with (a == B[a.index]); // {1,1,0}
В данном примере происходит поэлементое сравнение массивов A и B. Обрабатываемый массив имеет тип int []. Возвращаемый map() результат имеет тип bit [].
Соответствующие разделы стандарта: 7.12.5.
3. Множественные идентификаторы в ifdef
В новой версии стандарта был дополнен синтаксис директивы компилятора ifdef. Ранее директива могла обрабатывать лишь одиночные идентификаторы, теперь могут обрабатываться логические выражения из множественных идентификаторов.
Пример-сравнение старой и новой версий стандарта.
В старой версии стандарта:
`ifdef A
`ifdef B
// code for AND condition
`endif
`endif
В новой версии стандарта:
`ifdef (A && B)
// code for AND condition
`endif
Обратите внимание, что в новой версии стандарта также актуальна и первая запись. А вообще - удобно! Доступные логические операторы: &&, ||, ->, <->.
Кстати, первые два нововведения (а на самом деле и все последующие после этого) относятся к несинтезируемому подмножеству. Чего не скажешь про ifdef, который может использоваться и в синтезируемом коде. Как думаете, когда "завезут" в ПО для синтеза? Делаем ставки, господа.
Соответствующие разделы стандарта: 22.6.
4. Multiline strings
В новой версии стандарта была добавлена возможность размещать текст типа string на нескольких строках без использования специальных символов. Для этого используются тройные кавычки """.
Например:
$display("Humpty Dumpty sat on a wall.\n\
Humpty Dumpty had a great fall.");
Эквивалентно:
$display("""Humpty Dumpty sat on a wall.
Humpty Dumpty had a great fall. """);
То есть элемент типа string в тройных кавычках как бы автоматически "генерирует" необходимые символы для переноса на другую строку. Интересно также, что внутри тройных кавычек можно использовать одиночные " без специальных символов.
Например:
$display("Humpty Dumpty sat on a \"wall\".\n\
Humpty Dumpty had a great fall.");
Эквивалентно:
$display("""Humpty Dumpty sat on a "wall".
Humpty Dumpty had a great fall. """);
Вообще мне введение такого форматирования очень напомнило Python, где оно также присутствует. И кстати, оно часто используется для документирования кода. И тут я задумался...
А что, если:
function int add(int a, int b);
"""
This function adds two integers.
Arguments:
int a - first integer to add;
int b - second integer to add.
"""
return a + b;
endfunction
Соответствующие разделы стандарта: 5.9.
5. Использование чисел с плавающей точкой в функциональном покрытии
В новой версии стандарта теперь возможно определение функционального покрытия для чисел с плавающей точкой (real, shortreal).
Рассмотрим одну из проблем определения функционального покрытия для чисел с плавающей точкой. Разделы покрытия представляют собой конечные наборы значений. Для целых чисел такой набор может быть однозначно определен.
int a;
covergroup a_cg;
a_cp: coverpoint a {
bins b1 [] = {[0:9]}; // {0}, ..., {9}
}
endgroup
Числа же с плавающей точкой хранятся в памяти с некоторой точностью, что приводит к тому, что два разных числа могут быть представлены одним и тем же набором бит. Такая особенность может являться причиной дублирования значений в разделах покрытия, а также пересечения разделов покрытия.
real a;
covergroup a_cg;
a_cp: coverpoint a {
bins b1 [] = {[0:9]}; // ???
}
endgroup
В новой версии стандарта для поддержки использования чисел с плавающей точкой в функциональном покрытии была добавлена новая опция real_interval, а также новый синтаксис вида +/- и +%-, которые призваны решить проблемы с точностью при помощи явного указания минимального шага между "соседними" числами. Давайте разбираться, как это работает.
Пример:
real a;
parameter real VALUE = 50.0;
covergroup a_cg;
option.real_interval = 0.01;
a_cp: coverpoint a {
// [49.9:50.1]
bins b1 = {[VALUE+/-0.1]};
// [49.5:50.5]
bins b2 = {[VALUE+%-1.0]};
// [0.75:0.76), [0.76:0.77), ... [0.84:0.85]
bins a1 [] = {[0.75:0.85]};
}
endgroup
В данном примере раздел b1 покрывает значения от 49.9 до 50.1. Синтаксис +/- определяет отклонение от 50.0 на 0.1 (абсолютное значение) в обе стороны. Раздел b2 в свою очередь покрывает значения от 49.5 до 50.5. Синтаксис +%- определяет отклонение от 50.0 на 1% (относительное значение) в обе стороны, то есть на 0.5 в абсолютном выражении.
Для массива разделов a1 количество элементов в нем определяется опцией real_interval, которая задает шаг разбиения диапазона чисел с плавающей точкой. В данном примере шаг равен 0.01, так что интервал от 0.75 до 0.85 будет равномерно разбит на 10 интервалов: [0.85:0.86), [0.86:0.87), ..., [0.84:0.85]. Обратите внимание на включение границ в интервалы.
Соответствующие разделы стандарта: 19.5.1, 19.7.1.
6. Использование метода в качестве промежуточного результата
В большинстве симуляторов SystemVerilog в настоящее время поддерживается выполнение следующего примера:
module test;
class my_class;
function void print();
$display("Inside my_class!");
endfunction
endclass
function my_class get_my_class();
my_class cl;
cl = new();
endfunction
initial begin
get_my_class().print();
end
endmodule
Результат функции get_my_class() используется в качестве промежуточного результата, который содержит указатель на объект типа my_class, у которого вызывается метод print(). В англоязычной литературе это называют "chaining of method calls".
Не трудно догадаться, что результатом выполнения будет:
Inside my_class!
Хоть данный пример и поддерживается многими симуляторами, в стандарте явно не обозначались требования к соответствующему функционалу. В новой версии наконец-то появилось соответствующее описание. Из интересного: появилось правило разграничения иерархического обращения и промежуточного результата.
Например:
class A;
int member=123;
endclass
module top;
A a;
function A F(int arg=0);
int member;
a = new();
return a;
endfunction
initial begin
$display(F.member);
$display(F().member);
end
endmodule
Результатом выполнения будет:
0
123
Новая версия стандарта явно указывает на то, что при использовании функции в качестве промежуточного результата всегда должны использоваться (), даже если у функции нет аргументов. В данном же примере выражение F.member обращается к статической переменной member функции F, а F().member обращается к полю member объекта класса, который возвращается вызовом функции F.
Соответствующие разделы стандарта: 13.4.1.
7. ref static
Для начала нам необходимо вспомнить, что переменная в SystemVerilog может быть автоматической. Если говорить простым языком, то это значит, что её присутствие в памяти на протяжении всего времени симуляции не гарантируется.
Для автоматических переменных стандартом накладываются определенные ограничения на их использование. Например, переменная не может стоять слева от <= (быть целью Non-blocking assignment). Или, что более интересно, автоматическая переменная, объявленная вне блоков fork-join_any и fork-join_none не может быть использована внутри этих блоков.
Например:
initial begin
for (int j = 1; j <= 3; ++j) begin
fork
begin
automatic int k = j;
#k $write("%0t %0d\n", $time(), k);
end
join_none
end
end
Такая конструкция абсолютно легальна. Выводом будет:
1 1
2 2
3 3
А вот другой пример:
module test;
function void print(ref logic arg);
fork
forever @(arg) $display("arg changed %0t", $time());
join_none
endfunction
logic C;
initial begin
print(C);
#10; C = 1;
#10; C = 2;
end
endmodule
Который запрещен стандартом, так как для компилятора при анализе функции print() переменная arg потенциально может быть автоматической, а её использование внутри fork-join_none запрещено. Обратите внимание, что, даже если переменная, передаваемая в задачу будет статической, компилятор все равно сгенерирует ошибку.
Результатом запуска примера выше в QuestaSim 2021.2 будет:
vlog test.sv
** Error (suppressible): test.sv(4): (vlog-13300)
The task or function 'print' with ref argument 'arg' must be automatic.
А вот в новой версии стандарта существует возможность передавать статическую переменную в подобную функцию по ссылке. Теперь аргумент можно будет объявить как ref static.
Пример:
module test;
function void print(ref static logic arg);
fork
forever @(arg) $display("arg changed %0t", $time());
join_none
endfunction
logic C;
initial begin
print(C);
#10; C = 1;
#10; C = 2;
end
endmodule
Результат симуляции ожидается таким:
arg changed 10
arg changed 20
И знаете что. Симулятора, официально поддерживающего все нововведения SystemVerilog 2023 нет, а ожидаемый результат выполнения (ну почти) мне получить удалось. Спросите как?
Компиляцию первого примера в QuestaSim 2021.2 можно запустить с особыми флагами:
vlog test.sv -suppress 13300 -suppress 13219
-- Compiling module test
Errors: 0, Warnings: 0, Suppressed Errors: 1
Что это за флаги - уже другая история. Но результат симуляции был следующим:
arg changed 10
arg changed 20
Кто-то заранее "подложил соломку"? Вопрос, скорее, риторический.
Соответствующие разделы стандарта: 9.3.2, 13.5.2.
Заключение
Вот и все, дорогие читатели. Только что мы с вами погрузились в новую версию стандарта SystemVerilog. Как думаете, хороши нововведения? Спасибо вам за уделенное время! Всего наилучшего и до новых встреч!
А больше подобных обзоров вы можете найти в Telegram-канале автора Verification For All.
Post Scriptum
Если вам известны иные нововведения версии стандарта SystemVerilog 2023 года, то не стесняйтесь написать об этом в комментариях или в личные сообщения автору в Telegram. Также автору было бы интересно узнать мнение читателей по поводу того, какого еще функционала, по их мнению, не хватает в теперь уже самом актуальном стандарте языка SystemVerilog IEEE 1800-2023.
Demystifying UVM: Фабрика, часть 1
- Demystifying UVM: Фабрика, часть 1
Вступление
Цикл статей 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.
Для получения перечисленных выше знаний рекомендуется ознакомиться с лекцией автора в Школе синтеза цифровых схем: ООП в функциональной верификации. Транзакционная модель тестирования.
От автора про UVM
Версионирование
Библиотека UVM впервые была официально представлена в феврале 2011 года и со временем претерпевала изменения в различных версиях. Полный список версий и их исходный код представлены в соответствующем разделе на сайте компании Accellera.
В цикле статей Demystifying UVM для анализа будет использован исходный код UVM 1.2. Основной причиной является то, что, хоть версия 1.2 и была выпущена в далеком 2014 году, она по сей день является самой часто используемой и поддерживается всеми коммерческими симуляторами (в данном контексте "поддерживается" — предоставляется в уже скомпилированном виде)1. Стоит также заметить, что основные концепции, которые будут рассматриваться в циклей статей Demystifying UVM, были представлены уже в самой первой версии (1.0) и не претерпели практически никаких изменений вплоть до актуальной на момент написания статьи версии 2020-3.1. Исходные файлы библиотеки UVM версии 1.2 можно скачать на сайте компании Accellera по ссылке.
Сложность изучения
В ходе изучения UVM основными сложностями для инженера являются:
- высокий порог входа;
- внушительная и сложновоспринимаемая кодовая база;
- сравнительно небольшое количество профильной литературы.
Перед освоением UVM инженеру необходимо изучить множество конструкций SystemVerilog, а также принципы ООП и их реализацию. И, даже освоив вышеперечисленное, инженер столкнется с кодовой базовой из более 30000 строк кода, которая к тому же не всегда задокументирована.
Также стоит заметить, что хоть библиотека UVM и предоставляет действительно широкий спектр возможностей для верификации, в реальности используется лишь их небольшая часть со специфичными для конкретного проекта "вставками" функционала библиотеки.
Отдельным камнем преткновения является небольшое количество профильной литературы по UVM, в особенности той, в которой бы детально описывались основные концепции и механизмы работы. Известная UVM Cookbook по своей сути является сборником советов и шаблонов, что, тем не менее, нисколько не умаляет иных достоинств данной книги.
Подход автора к изучению
Сочинять не так уж трудно; зачеркивать лишние ноты – вот что труднее всего.
Иоганнес Брамс
Автор искренне убежден, что для разбора не самых простых концепций, прежде всего, необходимо отбросить все лишнее. И в исходном коде UVM "лишнего" действительно достаточно. Важно заметить, что под "лишним" в данном контексте подразумевается код, который никак не влияет на разбираемый функционал и который можно исключить в угоду удобству изучения.
Обращу ваше внимание, что под "исключить" имеется в виду буквальное исключение. В цикле Demystifying UVM будет анализироваться авторская версия библиотеки UVM, в которой сохранен лишь код, непосредственно отвечающий за разбираемый функционал. Это позволит начинающим инженерам сфокусироваться на конкретных концепциях, а после, при желании, изучить оригинальные исходные файлы UVM, которые можно скачать на сайте компании Accellera по ссылке.
Насколько хороша описанная выше задумка, дорогие читатели, — покажет время ваша обратная связь. А теперь — к делу!
SystemVerilog и параметризация классов типами
Перед погружением в исходный код UVM необходимо вспомнить одну из особенностей SystemVerilog. А именно — возможность параметризации класса типом. Пример параметризованного таким образом класса представлен в файле src/test/test_pkg.sv.
class my_wrapper #(type T);
static function T create_some_class();
T cl = new();
return cl;
endfunction
endclass
Класс my_wrapper параметризован типом T и содержит статический метод create_some_class(), функционалом которого является создание объекта типа T при помощи T cl = new() и возвращение указателя на него при помощи return cl.
Также в файле объявлены два класса для демонстрации параметризации типом:
class my_awesome_class;
virtual function void print();
$display("Hello from 'my_awesome_class'!");
endfunction
endclass
class my_new_awesome_class extends my_awesome_class;
virtual function void print();
$display("Hello from 'my_new_awesome_class'!");
endfunction
endclass
Обратите внимание, что так же, как и с "классическими" параметрами, my_wrapper#(my_awesome_class) и my_wrapper#(my_new_awesome_class) являются разными типами. Пример использования параметризованного класса представлен в файле src/test/tb_simple.sv.
initial begin
my_awesome_class cl;
cl = my_wrapper#(my_awesome_class)::create_some_class();
cl.print();
cl = my_wrapper#(my_new_awesome_class)::create_some_class();
cl.print();
end
В данном примере при помощи статического метода create_some_class() создается сначала объект типа my_awesome_class, а затем типа my_new_awesome_class. Handle cl будет указывать на объект типа my_awesome_class, а далее на объект типа my_new_awesome_class. При вызове метода print() сначала будет вызвана реализация класса my_awesome_class, а затем класса my_new_awesome_class, т.к. метод является виртуальным. Результат выполнения представлен ниже.
# Hello from 'my_awesome_class'!
# Hello from 'my_new_awesome_class'!
Для запуска примера при помощи QuestaSim или Verilator в директории src необходимо выполнить скрипты run_questa.sh и run_verilator.sh соответственно (с аргументом tb_simple):
run_questa.sh tb_simple
run_verilator.sh tb_simple
Лог запуска будет выведен в консоль, а артефакты симуляции сохранены в директорию src/out.
Создание компонентов в UVM
Case study
Типовое создание компонента в UVM выглядит следующим образом:
monitor = apb_monitor::type_id::create("mon", this);
У каждого начинающего (и не только) верификатора строка выше вызовет вопрос: что такое type_id::create()? В абсолютном большинстве литературы инженер увидит лишь упоминание о том, что вызов type_id::create() является альтерантивой вызову new(), и он должен использоваться для создания компонентов верификационного окружения.
Почему для создания компонентов необходимо использовать type_id::create() и какие возможности предоставляет данный подход — рассмотрим далее!
Макросы регистрации
Предварительно изучив необходимую теорию и таким образом размявшись, начинаем наше погружение. При объявлении компонента UVM необходимо использовать макрос uvm_component_utils, который, как указано в большинстве источников, отвечает за регистрацию типа. Пример использования представлен ниже.
class base_test extends uvm_component;
`uvm_component_utils(base_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
...
endclass
Что такое регистрация типа и что же кроется за представленным макросом? Рассмотрим исходный код файла, в котором определен uvm_component_utils. Для этого откроем файл src/uvm/uvm_object_defines.svh:
`define uvm_component_utils(T) \
`m_uvm_component_registry_internal(T,T) \
`m_uvm_get_type_name_func(T) \
Видим, что макрос uvm_component_utils(T) параметризован переменной T и раскрывается в два других макроса: m_uvm_component_registry_internal(T,T) и m_uvm_get_type_name_func(T). Их исходный код приведен ниже.
`define m_uvm_component_registry_internal(T,S) \
typedef uvm_component_registry #(T,`"S`") type_id; \
static function type_id get_type(); \
return type_id::get(); \
endfunction
`define m_uvm_get_type_name_func(T) \
const static string type_name = `"T`"; \
virtual function string get_type_name (); \
return type_name; \
endfunction
Давайте раскроем макрос в примере выше:
class base_test extends uvm_component;
typedef uvm_component_registry #(base_test, "base_test") type_id;
static function type_id get_type();
return type_id::get();
endfunction
const static string type_name = "base_test";
virtual function string get_type_name();
return type_name;
endfunction
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
...
endclass
Если анализировать код снизу вверх, то макрос uvm_component_utils отвечает за создание в теле класса статического поля type_name, которое содержит название типа класса в виде строки, а также метод get_type_name(), возвращающий это название типа.
Также макрос определяет внутри класса тип type_id, который является алиасом для типа uvm_component_registry #(base_test, "base_test") и статический метод get_type(), который вызывает статический метод get() типа type_id.
Так что же значит type_id::create()? На самом деле это ничего более, чем вызов статического метода типа type_id, который является алиасом для класса регистрации uvm_component_registry, параметризованного типом класса T, который мы хотим зарегистрировать (в примере выше T = base_test).
Внимательно прочитайте предыдущее предложение и постарайтесь его осознать. Постарайтесь объяснить, почему для класса типа base_test вызов type_id::create() эквивалентен вызову uvm_component_registry#(base_test, "base_test")::create().
Визуализация вызова type_id::create() представлена на изображении ниже.
Но что же все-таки значит "зарегистрировать" тип, и что такое класс регистрации? Давайте обсудим!
Proxy-класс регистрации
Рассмотрим исходный код файла, в котором определен тип uvm_component_registry. Для этого откроем файл src/uvm/uvm_registry.svh:
class uvm_component_registry #(
type T = uvm_component,
string Tname = "<unknown>"
) extends uvm_object_wrapper;
...
local static this_type me = get();
static function this_type get();
if (me == null) begin
me = new();
end
return me;
endfunction
...
endclass
Класс типа uvm_component_registry является proxy-классом для типа, которым он параметризован (T). По своей сути proxy-класс является своеобразным хранилищем информации о типе. Также proxy-класс предоставляет API для создания объектов этого типа.
Заметим, что proxy-класс наследуется от класса uvm_object_wrapper. Данный класс не представляет особого интереса и является непараметризованным классом, через handle которого можно передавать указатель на параметризованные uvm_component_registry.
Вернемся к объявлению класса uvm_component_registry. Важно заметить, что класс является singleton-классом2. Особенность singleton'а заключается в том, что объект такого типа в ходе симуляции существует в единственном экземпляре и создается при первом обращении к нему при помощи специализированного статического метода. Название такого метода может быть любым, но часто можно встретить такие наименования, как get() или get_inst().
Давайте разберем чуть подробнее тело класса выше. Статическая переменная me является защищенным указателем на единственный экземпляр singleton-класса. Метод get() является методом доступа к этому защищенному указателю. Обратите внимание, что указатель инициализируется единственный раз в том случае, если он равен null (не указывает ни на какой объект в памяти).
Концептуально использование singleton-класса представлено на изображении ниже.
Каждый компонент (в примере это component_a, component_b и component_e, они выделены зеленым цветом) при обращении к singleton-классу uvm_component_registry ,будет получать указатель единственный экземпляр этого класса. Указатель будет храниться в защищенном поле me.
Проанализируем другую часть тела класса регистрации:
class uvm_component_registry #(
type T = uvm_component,
string Tname = "<unknown>"
) extends uvm_object_wrapper;
...
virtual function uvm_component create_component(
string name,
uvm_component parent
);
T obj;
obj = new(name, parent);
return obj;
endfunction
...
endclass
Метод create_component() возвращает объект типа T (или объект типа, наследуемого от T). Именно в этом методе мы можем увидеть непосредстевенно вызов конструктора new(). То есть именно в данном методе создается объект запрашиваемого типа T. Это значит, что вызов type_id::create() приводит к вызову create_component() proxy-класса типа type_id, ведь type_id — это просто алиас для proxy-класса, параметризованного типом T. Но где вызывается этот метод? Скоро узнаем!
Проанализируем другую часть тела класса регистрации:
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
Вот мы и добрались до метода create()! Именно этот статический метод вызывается при помощи type_id::create(). Погрузимся еще глубже. В данном методе нас интересуют строки:
uvm_coreservice_t cs = uvm_coreservice_t::get();
uvm_factory factory = cs.get_factory();
obj = factory.create_component_by_type(get(), name, parent);
В первой строке реализуется доступ к singleton-классу типа uvm_coreservice_t. Данный класс предоставляет API для создания объектов, настроек логирования и т.д., его реализация будет рассмотрена чуть позже. Во второй строке при помощи указателя на класс сервисов производится получение доступа к указателю на класс фабрики UVM (uvm_factory) при помощи cs.get_factory(). И, наконец, в 3 строке производится создание объекта при помощи create_component_by_type().
Можно сделать вывод, что вызов type_id::create() приводит к вызову метода create_component_by_type() класса фабрики UVM (uvm_factory). Доступ же к классу фабрики производится через класс сервисов UVM (uvm_coreservice_t). Также теперь становится очевидно, что каждый вызов type_id::create() приводит к получению указателя на единственный экземпляр proxy-класса при помощи get().
Дополненная визуализация вызова type_id::create() представлена на изображении ниже.
Какова реализация класса фабрики и класса сервисов в UVM? Давайте обсудим это в разделах ниже!
Класс сервисов UVM
Рассмотрим исходный код файла, в котором определен тип uvm_coreservice_t. Для этого откроем файл src/uvm/uvm_coreservice.svh:
class uvm_coreservice_t;
local static uvm_coreservice_t inst;
static function uvm_coreservice_t get();
if(inst == null) begin
inst = new();
end
return inst;
endfunction
local uvm_factory factory;
virtual function uvm_factory get_factory();
if(factory == null) begin
factory = new();
end
return factory;
endfunction
endclass
Уже вооружившись знаниями о singleton-классах, делаем вывод о том, что данный класс также является singleton. Доступ к единственному экземпляру осуществляется при помощи статического метода get(). Обратим внимание, что класс содержит указатель на класс фабрики UVM в защищенном поле factory. Доступ к этому указателю осуществляется при помощи метода get_factory(). Стоит заметить, что в оригинальной версии (не авторской) класс сервисов содержит больше функционала, однако в рамках данной статьи дополнительный код осложнил бы восприятие.
Дополненная визуализация вызова type_id::create() представлена на изображении ниже.
To be continued...
Вот и подошла к своему логическому завершению первая статья цикла Demystifying UVM. В ней мы узнали, что классы в SystemVerilog можно параметризовывать типами, и запустили простейший пример. Узнали, что кроется за макросом uvm_component_utils и что такое proxy-класс регистрации. Освоили концепцию singleton-класса и проанализировали минимальную авторскую реализацию класса сервисов UVM.
В конце данной статьи мы вплотную приблизились к реализации класса фабрики UVM. Она будет подробно разобрана в следующей статье, ссылка на которую в скором будет опубликована в Telegram-канале автора Verification For All (VFA). Вас ждет подробный разбор механизмов создания компонентов UVM и переопределения их типов, а также множество примеров.
Всего вам наилучшего, дорогие читатели! Мира и процветания!☀️
Siemens QuestaSim, Cadence Xcelium, Synopsys VCS. Что интересно: UVM 1.2 единственная версия UVM, которая поддерживается встроенным симулятором Xilinx Vivado.
Ссылка на отличную статью о singleton-классах в SystemVerilog.
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).
SystemVerilog Gotchas, Tips and Tricks, часть 1
- SystemVerilog Gotchas, Tips and Tricks, часть 1
Вступление
Цикл статей SystemVerilog Gotchas, Tips and Tricks
Доброго времени суток, дорогие читатели! Данная статья является первой в целом цикле, который называется SystemVerilog Gotchas, Tips and Tricks. В нем я буду делиться интересными особенностями языка SystemVerilog и тем, как они могут проявляться в ходе функциональной верификации и как по возможности избежать негативных последствий этих проявлений.
Motivation или зачем это всё
Именно так: бед нет — есть лишь стечение более сложных и по-своему более критических обстоятельств. Я так и определяю для себя беду: стечение сложных обстоятельств.
Юрий Петрович Власов
Инженер в процессе обучения или разработки может сталкиваться с множеством препятствий. Одними из самых трудозатратных являются те, что связаны со скрытыми от поверхностного взгляда особенностями языка SystemVerilog. Для их преодоления могут потребоваться часы размышлений, отладки и чтения стандарта.
Автор имеет обширный опыт обучения начинающих и со временем пришел к выводу, что подавляющая часть ошибок практически один в один копируется от инженера к инженеру. Разумеется, не в прямом смысле. Тем не менее, каждый юный верификатор сталкивается с абсолютно типичным набором проблем.
Цель создания данного цикла статей — предупредить ошибки новичка и сохранить его ментальное здоровье. А если более серьезно и глобально — создать пособие, к которому можно будет обратиться в случае столкновения с новым и совершенно непонятным поведением верификационного окружения. Получится ли? Покажет время ваша обратная связь!
О запуске примеров
Для запуска примеров используется используется Siemens QuestaSim и, где это возможно, Verilator.
Рандомизация переменных ширины более 32 бит
Функции $urandom() и $urandom_range() в SystemVerilog возвращают случайный unsigned int, разрядность которого 32 бита. Так что рандомизация переменных шириной более 32 бит за 1 вызов этих функций невозможна (будет срандомизирована лишь младшая 32-битная часть).
typedef bit [63:0] data_t;
initial begin
data_t data;
repeat(3) begin
data = $urandom(); $display("%h", data);
end
end
Результат запуска симуляции:
00000000ce46aa23
00000000a01b6e32
000000005cb53a0a
000000008b4e9f1d
00000000a80385b8
Для рандомизации переменных шириной более 32 бит можно использовать либо конкатенацию $urandom()/$urandom_range(), либо функцию std::randomize().
typedef bit [63:0] data_t;
initial begin
data_t data;
repeat(3) begin
data = {$urandom(), $urandom()}; $display("%h", data);
void'(std::randomize(data)); $display("%h", data);
end
end
Результат запуска симуляции:
ce46aa23a01b6e32
8b4e9f1d5cb53a0a
a80385b869968eda
e0a228ac13c11ac0
913a793217e9241c
12f806fda3ea30be
Пример с комментариями: src/test/rand_width.sv.
Запуск примера в QuestaSim:
./run_questa.sh rand_width
На момент написания статьи Verilator (v5.035) не поддерживает std::randomize(). Соответствующий issue.
Индексация данных при упаковке
При упаковке данных в SystemVerilog часто можно столкнуться с тем, что индексы, которые определяют интервал бит, должны быть compile-time или elaboration-time константами. То есть их значения должны быть известны симулятору на этапе компиляции или элаборации, которые предшествуют этапу симуляции. Иными словами, значения индексов не могут динамически изменяться в ходе симуляции.
То есть нет возможности написать, например, конструкцию:
byte data [];
longint word;
for(int i = 0; i < 8; i = i + 1) begin
word[i] = data[(i+1)*8-1:i*8];
end
Для упаковки данных в SystemVerilog можно использовать streaming operator:
byte data [];
longint word;
initial begin
data = new[8]('{8'hfa, 8'hde, 8'hca, 8'hfe,
8'hde, 8'had, 8'hbe, 8'hef});
word = {>>{data}};
$display("word: %h", word);
word = {<<8{data}};
$display("word: %h", word);
end
Результат запуска симуляции:
fadecafedeadbeef
efbeaddefecadefa
Подробнее о streaming operator можно узнать в статье How to Pack Data Using the SystemVerilog Streaming Operators от Amiq Consulting и в стандарте SystemVerilog IEEE Std 1800-2023 в разделе 11.4.14.
Для упаковки данных в SystemVerilog также можно использовать indexed part-select:
byte data [];
longint word;
initial begin
data = new[8]('{8'hfa, 8'hde, 8'hca, 8'hfe,
8'hde, 8'had, 8'hbe, 8'hef});
foreach(data[i]) word[64-8*(i+1)+:8] = data[i];
$display("word: %h", word);
foreach(data[i]) word[8*i+:8] = data[i];
$display("word: %h", word);
end
Результат запуска симуляции:
fadecafedeadbeef
efbeaddefecadefa
Подробнее об indexed part-select можно узнать в стандарте SystemVerilog IEEE Std 1800-2023 в разделе 11.5.1.
Пример с комментариями: src/test/data_packing.sv.
Запуск примера в QuestaSim:
./run_questa.sh data_packing
На момент написания статьи Verilator (v5.035) некорректно определяет очередность данных для упаковки при помощи streaming operator. Автор создал issue.
UPD 17.10.25: Issue был успешно закрыт, исправления попали в ветку master. Протестировано на версии 5.040 2025-08-30 rev v5.040.
Запуск примера в Verilator:
./run_verilator.sh data_packing
Завершение потоков через disable fork
Пример ниже состоит из задачи run_processes() и блока initial begin-end, который запускает эту задачу. Выход из задачи происходит после завершения процесса 1. Процесс 2 при этом продолжит выполняться. Далее в блоке запускаются процессы 3 и 4. Выполнение кода продолжается после завершения процесса 3. Далее вызывается disable fork, после чего запускается процесс 5.
task run_processes();
fork
repeat( 3) #1 $display("Process 1");
repeat(20) #1 $display("Process 2");
join_any
$display("-- Exit run_processes()");
endtask
initial begin
run_processes();
fork
repeat( 3) #1 $display("Process 3");
repeat(10) #1 $display("Process 4");
join_any
$display("-- Exit fork and disable it");
disable fork;
repeat(3) #1 $display("Process 5");
$finish();
end
Результат запуска симуляции:
Process 1
Process 2
Process 1
Process 2
Process 1
-- Exit run_processes()
Process 2
Process 3
Process 4
Process 2
Process 3
Process 4
Process 2
Process 3
-- Exit fork and disable it
Process 5
Process 5
Process 5
После disable fork процесс 2 перестает выполняться, как и 4. Остается только 5. Получается, что disable fork завершает не только процессы "ближайшего" fork, как склонны полагать начинающие. Так на какие же процессы влияет disable fork? Давайте обратимся к разделу 9.6.3. стандарта SystemVerilog IEEE Std 1800-2023.
The disable fork statement terminates all descendant subprocesses (not just immediate children) of the calling process, including descendants of subprocesses that have already terminated.
Оказывается, что disable fork завершает все дочерние процессы вызывающего disable fork процесса. То есть в нашем случае все процессы, созданные в initial, в том числе процессы, которые были созданы при вызове задачи run_processes(), так как она вызывалась в этом initial.
Пример с комментариями: src/test/disable_fork.sv.
Запуск примера в QuestaSim:
./run_questa.sh disable_fork
Запуск примера в Verilator:
./run_verilator.sh disable_fork
Для безопасного завершения процессов через disable fork можно использовать особую обертку fork begin - end join:
initial begin
run_processes();
fork begin
fork
repeat( 3) #1 $display("Process 3");
repeat(10) #1 $display("Process 4");
join_any
$display("-- Exit fork and disable it");
disable fork;
end join
repeat(3) #1 $display("Process 5");
$finish();
end
Дополнительный fork-join создает новый процесс, в котором запускаются процессы 3 и 4, а также вызывается disable fork, который принудительно завершит только процесс 4. Процесс 2 является дочерним процессом initial, а не процесса, созданного в fork begin - end join, так что он завершен не будет.
Результат запуска симуляции:
Process 1
Process 2
Process 1
Process 2
Process 1
-- Exit run_processes()
Process 2
Process 3
Process 4
Process 2
Process 3
Process 4
Process 2
Process 3
-- Exit fork and disable it
Process 2
Process 5
Process 2
Process 5
Process 2
Process 5
SystemVerilog concurrent assertions и event regions
Начнем с примера
Необходимо проверить заведомо сломанный модуль знакорасширения.
module signextend(
input logic clk_i,
input logic aresetn_i,
input logic [ 7:0] data_i,
output logic [31:0] data_o
);
always_ff @(posedge clk_i or negedge aresetn_i) begin
if(!aresetn_i) begin
data_o <= 'b0;
end
else begin
// NOTE: вычисление знакорасширения
// сломано в данном примере.
data_o <= {{23{data_i[7]}}, data_i};
end
end
endmodule
Напишем SystemVerilog property и инициируем его проверку через assert property:
property pData;
logic [7:0] data;
@(posedge clk_i) disable iff(!aresetn_i)
(1, data = data_i) ##1 data_o === {{24{data[7]}}, data};
endproperty
apData: assert property(pData) else begin
$error("data_i was: %h, data_o is: %h",
$past(data_i, 1), $sampled(data_o));
end
Каждый такт значение со входа data_i сохраняется во временную переменную data (да, в теле property можно объявлять переменные!). На следующем такте значение data_o сравнивается с знакорасширенным значением data.
Если знакорасширенное значение data на совпадает со значением data_o (о чем автор любезно позаботился, сломав модуль), то генерируется пользовательская ошибка. У читателя может возникнуть вопрос: что за конструкции языка $past(data_i, 1) и $sampled(data_o)? Не забегайте вперед! Дайте автору начать чуть издалека...
Пример с комментариями: src/test/sva_event_regions.sv.
Запуск примера в QuestaSim в режиме GUI:
./run_questa.sh sva_event_regions -gui
На момент написания статьи Verilator (v5.035) не поддерживает объявление переменной в теле property.
Результат запуска симуляции:
# ** Error: data_i was: a4, data_o is: 7fffffa4
# Time: 75 ns Started: 65 ns Scope: sva_event_regions.apData
# ** Error: data_i was: c6, data_o is: 7fffffc6
# Time: 85 ns Started: 75 ns Scope: sva_event_regions.apData
# ** Error: data_i was: 9e, data_o is: 7fffff9e
# Time: 95 ns Started: 85 ns Scope: sva_event_regions.apData
Посмотрим на первое сообщение об ошибке. Оно гласит, что в момент 65ns значение data_i было равно a4, а в момент 75ns его знакорасширенное значение data_o равно 7fffffa4. Генерация ошибки ожидаема, так как модуль заведомо сломан автором.
Давайте внимательно взглянем на временную диаграмму:

В моменты времени 65ns и 75ns вход data_i и выход data_o меняются по положительному фронту clk_i. Моменты изменения сигналов в ходе симуляции SystemVerilog уже принесли и продолжают приносить ощутимые неприятности инженерам по всему миру. И concurrent assertions — не исключение. Давайте подробнее!
О SystemVeriog event regions
На изображении ниже представлена авторская упрощенная версия схемы регионов выполнения, которые последовательно выполняются в рамках каждого момента времени симуляции, содержащего события. Подробнее про регионы выполнения можно узнать в стандарте SystemVerilog IEEE Std 1800-2023 в разделе 4. Также данная тема освещена в одной из лекций автора в школе синтеза цифровых схем, и еще более подробно уже в другой лекции автора там же.
Из предыдущего момента времени (time slot) симулятор переходит к preponed регион текущего момента времени. В данном регионе в том числе считываются значения переменных для проверки в concurrent assertions (см. SystemVerilog IEEE Std 1800-2023 раздел 16.5.1). Фиксируем: значения переменных для проверки в concurrent assertions считываются в самом начале момента времени.
Через несколько иных регионов выполняется Active регион, где в том числе рассчитываются значения в комбинационной логике. После еще нескольких регионов в NBA (Nonblocking Assignment) регионе значения с комбинационной логики защелкиваются в последовательностную.
После выполнения других регионов после NBA симулятор попадает в Reactive регион, где, при наличии ошибки в concurrent assertion, генерирует либо implementation-specific ошибку, либо выполняет пользовательский код в ветви else в assert property, если такая ветвь присутствует (см. SystemVerilog IEEE Std 1800-2023 раздел 16.4.1). Присутствие ветви else — как раз случай из представленного ранее примера!
Итак, вооружившись теорией, продолжаем разбирать пример.
Системные функции $past() и $sampled()
Вернемся к временной диаграмме. Продублирую ее ниже для удобства читателя.

Вспомним, что в моменты времени 65ns и 75ns вход data_i и выход data_o меняются по положительному фронту clk_i.
Обратим внимание на момент времени 65ns. Вход data_i меняет свое значение с a4 на c6. Для проверки в property фиксируется значение a4 в Preponed регионе. Значение c6 защелкнется в data_i в NBA регионе, который идет после Preponed.
Обратим внимание на момент времени 75ns. Выход data_o меняет свое значение с 7fffffa4 на 7fffffс6. Для проверки в property по аналогии с моментом времени 65ns фиксируется значение 7fffffa4.
Проверка не проходит (модуль сломан автором), ведь знакорасширенное значение должно быть ffffffa4. Это отражено в результате запуска:
# ** Error: data_i was: a4, data_o is: 7fffffa4
# Time: 75 ns Started: 65 ns Scope: sva_event_regions.apData
Давайте попробуем убрать $sampled() из кода ошибки:
apData: assert property(pData) else begin
$error("data_i was: %h, data_o is: %h",
$past(data_i, 1), data_o);
end
Результат запуска симуляции:
# ** Error: data_i was: a4, data_o is: 7fffffс6
# Time: 75 ns Started: 65 ns Scope: sva_event_regions.apData
Интересно! Значение data_o в выводе изменилось на 7fffffc6, которое соответствует результату для значения data_i, равного c6. Но, зная теорию из раздела выше, мы можем объяснить такое певедение симулятора. Ведь выполнение ветви else происходит в Reactive регионе, после обновление выхода data_o в NBA регионе. Соответственно значение data_o для вывода берется уже обновленное!
Так какую же роль играет системная функция $sampled(). Не трудно догадаться — функция возвращает значение переменной в Preponed регионе. То значение, которое было использовано при в проверке в concurrent assertion. Такие значения в контексте assertions в стандарте SystemVerilog называются сэмплированными (англ. sampled).
С системной функцией $past() все еще проще. Вызов $past(val, n) возвращает значение переменной val, сэмплированное на n тактов во времени ранее. Совет: при генерации пользовательских ошибок в concurrent assertions используйте $sampled() и $past() для вывода значений переменных.
Тонкости fork-join_none
Несовершенство обучающих материалов
В подавляющем большинстве обучающих материалов по SystemVerilog (в особенности открытых) про fork-join_none можно увидеть формулировку, похожую на:
As in the case of
fork-joinandfork-join_anyfork block is blocking, but in case offork-join_nonefork block will be non-blocking. Processes inside thefork-join_noneblock will be started at the same time, fork block will not wait for the completion of the process inside thefork-join_none.
или:
A
forkandjoin_nonewill allow the main thread to resume execution of further statements that lie after the fork regardless of whether the forked threads finish. If five threads are launched, the main thread will resume execution immediately while all the five threads remain running in the background.
Цитаты взяты с популярных тематических сайтов ChipVerify и VerificationGuide соответственно.
Из формулировок выше можно сделать вывод, что процессы, объявленные в fork-join_none, запускаются и выполняются совместно с выражениями в родительском процессе (в котором был объявлен fork-join_none).
К сожалению, формулировки выше, а, следовательно, и вывод из них, неверны. И вот почему!
Продолжим примером
module fork_join_none;
initial begin
$display("Statement 1");
fork
$display("Process 1");
join_none
$display("Statement 2");
$display("Statement 3");
$display("Statement 4");
end
endmodule
Исходя из формулировок выше вывод симулятора будет не определен, так как процесс $display("Process 1") запускается совместно с $display после join_none. Подробнее про недетерменированность можно узнать в стандарте SystemVerilog IEEE Std 1800-2023 в разделе 4.7.
Однако, любой симулятор большой тройки (Siemens QuestaSim, Synopsys VCS, Cadence Xcelium) выведет результат запуска симуляции:
Statement 1
Statement 2
Statement 3
Statement 4
Process 1
Да, разумеется, читатель может сказать, что симуляторы зачастую вносят дополнительную детерменированность там, где ее не должно быть. В этом случае, независимо от условий запуска (время, зерно рандомизации и т.д.), можно наблюдать одно и то же поведение, хотя согласно стандарту языка такое поведение не является обязательным. Однако это не наш случай, и сейчас я расскажу почему!
fork-join_none и стандарт SystemVerilog
Давайте обратимся к разделу 9.3.2. стандарта SystemVerilog IEEE Std 1800-2023. Внимателно посмотрим на таблицу 9-1.

Пока противоречий с формулировками выше не наблюдаются. Процесс, вызвавший, fork-join_none продолжает выполняться совместно с процессами, созданными в fork-join_none. Однако обратим внимание на предложение после таблицы. Как на зло оно еще и на другой странице...
In all cases, processes spawned by a fork-join block shall not start executing until the parent process is blocked or terminates.
Это очень важное замечание, которое гласит, что процессы в любом блоке fork-join не должны быть запущены до момента, пока процесс, вызвавший fork-join, не заблокируется или не завершится. Если с завершением все понятно, то что значит заблокируется? В данном контексте заблокируется — выполнит блокирующее выражение. Но что значит блокирующее выражение?
Согласно Annex P стандарта SystemVerilog IEEE Std 1800-2023:
blocking statement: A construct having the potential to suspend a process. This potential is determined through lexical analysis of the source syntax alone, not by execution semantics. For example, the statement wait(1) is considered a blocking statement even though evaluation of the expression '1' will be true at execution.
То есть блокирующее выражение — это выражение, которое, исходя из синтаксиса, может привести к остановке процесса. Это может быть wait(), @, # =. Также блокирующими выражениями являются join и join_any. Ведь они блокируют поток, вызвавший fork до завершения всех или одного процесса в fork соответственно.
Вернемся к уточнению в стандарте:
In all cases, processes spawned by a fork-join block shall not start executing until the parent process is blocked or terminates.
С fork-join и fork-join_any все понятно, они сами по себе блокируют родительский процесс, который их вызывал, а процессы в fork начинают выполняться. А что с fork-join_none?
И снова пример
module fork_join_none;
initial begin
$display("Statement 1");
fork
$display("Process 1");
join_none
$display("Statement 2");
$display("Statement 3");
$display("Statement 4");
end
endmodule
Однозначно определенный результат запуска симуляции:
Statement 1
Statement 2
Statement 3
Statement 4
Process 1
Такой результат обсусловлен тем, что процесс $display("Process 1") по стандарту должен начать выполняться в момент когда родительский initial начнет выполнять блокирующее выражение или завершится. В примере все $display() после join_none не являются блокирующими выражениями. Родительский initial завершится, после чего процесс в fork_join_none начнет свое выполнение. То есть вывод Process 1 будет сделан после всех остальных выводов в примере.
Пример с комментариями: src/test/fork_join_none.sv.
Запуск примера в QuestaSim:
./run_questa.sh fork_join_none
На момент написания статьи Verilator (v5.035) некорректно выполняет код примера, выполняя вывод Process 1 до завершения родительского initial. Автор создал issue.
Неужели это так важно?
Читатель может усомниться в необходимости знания таких тонкостей в поведении конструкций SystemVerilog и предположить, что в реальных проектах вероятность столкнуться с таким поведением крайне мала. К сожалению, это не так. Показательный пример представлен уже в следующем разделе.
Совместный запуск последовательностей на массиве агентов
Функциональная верификация межсоединений
В современных СнК для соединения множества функциональных блоков между собой используются межсоединения шин (англ. interconnect), которые маршрутизируют потоки данных между блоками. Типовая структурная схема СнК с межсоединением на основе интерфейса AXI4 представлена на изображении ниже.
Для верификации межсоединений в большинстве случаев проектируются верификационные окружения, имитирующие обмен данными между множеством функциональных блоков. Типовая структурная схема такого верификационного окружения представлена на изображении ниже.
На схеме компоненты типа Agent (Агент) получают высокоуровневые транзакции записи/чтения от Sequence (последовательностей) и отправляют их на входные порты тестируемого межсоединения DUT. Абстракции в виде агентов и последовательностей активно используются в универсальной методологии верификации (Universal Verification Methodology, UVM), о которой вы можете подробнее узнать в цикле статей автора Demistifying UVM.
Продолжим примером
Пример UVM-класса теста для верификации межсоединения представлен ниже.
class icon_test extends uvm_test;
`uvm_component_utils(icon_test)
icon_agent ag []; int ag_num;
icon_seq seq [];
...
virtual function void build_phase(uvm_phase phase);
void'(uvm_resource_db#(int)::read_by_name(
get_full_name(), "ag_num", ag_num));
ag = new[ag_num];
foreach(ag[i]) begin
ag[i] = icon_agent::type_id::create(
$sformatf("ag_%0d", i), this);
end
endfunction
virtual task main_phase(uvm_phase phase);
phase.raise_objection(this);
seq = new[ag_num];
foreach(seq[i]) begin
seq[i] = icon_seq::type_id::create(
$sformatf("seq_%0d", i));
end
foreach(seq[i]) begin
fork
int j = i;
seq[j].start(ag[j].sqr);
join_none
end
wait fork;
phase.drop_objection(this);
endtask
endclass
Класс содержит динамический массив агентов и последовательностей для них. В методе build_phase() создаются агенты в зависимости от настройки ag_num, полученной извне. В методе main_phase() создаются последовательности, и каждая из них запускается на своем агенте, после чего запускается ожидание завершения каждой из последовательностей при помощи wait fork. Код запуска последовательностей рассмотрим подробнее.
foreach(seq[i]) begin
fork
int j = i;
seq[j].start(ag[j].sqr);
join_none
end
wait fork;
Особый интерес представляет строка int j = i;, отвечающая за создание новой переменной, в которую копируется значение итератора. Но зачем это делать? Ведь итератор можно использовать напрямую, без промежуточной переменной. Но можно ли? Давайте обо всем по порядку!
Все последовательности запускаются совместно через fork-join_none для полноценной нагрузки межсоединения со стороны различных функциональных блоков. i-ая последовательность запускается на i-ом агенте.
Пример с комментариями: src/test/fork_join_seqs.sv.
Запуск примера в QuestaSim:
./run_questa.sh fork_join_seqs
На момент написания статьи Verilator (v5.035) не поддерживает UVM. Текущий статус поддержки можно посмотреть в соответствующем issue.
Результат запуска симуляции:
@ 0: uvm_test_top.ag_0.drv [drv] Got item: addr: f7, data: 13a33d07
@ 0: uvm_test_top.ag_1.drv [drv] Got item: addr: 50, data: ebf57900
@ 0: uvm_test_top.ag_2.drv [drv] Got item: addr: d1, data: 1864b414
@ 0: uvm_test_top.ag_3.drv [drv] Got item: addr: 10, data: 3c257344
@ 0: uvm_test_top.ag_4.drv [drv] Got item: addr: 73, data: 0c784afa
@ 0: uvm_test_top.ag_5.drv [drv] Got item: addr: 43, data: 8d248e59
@ 10: uvm_test_top.ag_5.drv [drv] Got item: addr: ba, data: 8aa33156
@ 12: uvm_test_top.ag_4.drv [drv] Got item: addr: 8a, data: 330f55a7
@ 16: uvm_test_top.ag_2.drv [drv] Got item: addr: 80, data: 02aa1ac7
@ 18: uvm_test_top.ag_3.drv [drv] Got item: addr: 1f, data: 26a9b0ca
@ 19: uvm_test_top.ag_0.drv [drv] Got item: addr: f9, data: 62c70ec5
@ 20: uvm_test_top.ag_1.drv [drv] Got item: addr: c0, data: 447628cb
Вложенный в класс агента класс драйвера информирует об обработке транзакций каждой из последовательностей. Обратите внимание, что транзакции обрабатываются разными агентами, это видно по их иерархическому имени. Также обратите внимание, что первые транзакции агенты получают одновременно в нулевой момент времени, это говорит об одновременной их отправке из последовательностей. Следующие транзакции агенты получают неодновременно из-за различных задержек взаимодействия с портами верифицируемого устройства.
Разбираемся с промежуточными переменными
Давайте уберем промежуточную переменную j:
foreach(seq[i]) begin
fork
seq[i].start(ag[i].sqr);
join_none
end
wait fork;
Результат запуска симуляции:
# ** Fatal: (SIGSEGV) Bad handle or reference.
# Time: 0 ns Iteration: 120 Process: /icon_dv_pkg::icon_test::main_phase
# Fatal error in Task icon_dv_pkg/icon_test::main_phase at ./test/fork_join_seqs.sv
Неожиданно получаем ошибку доступа к памяти. Но почему? Давайте раскроем цикл, приняв размерность массивов, равной 3.
int i = 0; // i = 0
fork
seq[i].start(ag[i].sqr);
join_none
i = i + 1; // i = 1
fork
seq[i].start(ag[i].sqr);
join_none
i = i + 1; // i = 2
fork
seq[i].start(ag[i].sqr);
join_none
i = i + 1; // i = 3
wait fork;
А теперь, руководствуясь знаниями об особенностях исполнения fork-join_none из предыдущего раздела, обоснуем ошибку. Запуск всех seq[i].start(ag[i].sqr) будет выполнен, как только родительский процесс заблокируется, то есть начнет выполнять блокирующее выражение. В нашем случае это выражение wait fork, которое отвечает за ожидание завершения процессов в fork. Каким же будет значение i в этот момент? Верно: i будет равно 3. Все вызовы seq[i].start(ag[i].sqr) выродятся в seq[3].start(ag[3].sqr). Если количество агентов равно 3, то какой максимальный индекс в массиве агентов? Тоже верно: он равен 2. Отсюда и ошибка доступа к памяти. Это ошибка доступа к несуществующему элементу массива.
А теперь вернем промежуточную переменную и снова раскроем цикл.
int i = 0; // i = 0
fork
int j = i; // j = 0
seq[i].start(ag[i].sqr);
join_none
i = i + 1; // i = 1
fork
int j = i; // j = 1
seq[i].start(ag[i].sqr);
join_none
i = i + 1; // i = 2
fork
int j = i; // j = 2
seq[i].start(ag[i].sqr);
join_none
i = i + 1; // i = 3
wait fork;
Обратимся к разделу 9.3.2 стандарта SystemVerilog IEEE Std 1800-2023:
Variables declared in the block_item_declaration of a fork-join block shall be initialized to their initialization value expression whenever execution enters their scope and before any processes are spawned.
Согласно выдержке выше, переменные, объявленные в fork-join блоке должны быть проинициализированы, как только симулятор в ходе выполнения "попадет" на выражение инициализации, то есть семантически его распознает. Таким образом, значения локальных переменных j каждого fork-join_none блока инициализируются еще до запуска процессов в них и сохраняют свое значение.
Во всех вызовах seq[j].start(ag[j].sqr) переменная j будет принимать соответственно значения 0, 1 и 2, что приведет к корректному запуску последовательностей на агентах.
To be continued...
Первая часть цикла SystemVerilog Gotchas, Tips and Tricks подошла к концу, дорогие читатели. Следите за обновлениями в Telegram-канале автора Verification For All (VFA)!
Всего вам наилучшего, дорогие читатели! Мира и процветания!☀️
SystemVerilog Gotchas, Tips and Tricks, часть 2
- SystemVerilog Gotchas, Tips and Tricks, часть 2
- О запуске примеров
- Вступление
- SystemVerilog
process - SystemVerilog и стабильность рандомизации
- Контекст
- Воспроизводимость тестов
- Генераторы случайных чисел (RNG) и их инициализация
- Стабильность рандомизации объектов
- Симуляторы тоже могут ошибаться
- Стабильность рандомизации потоков
- Симуляторы вновь ошибаются
- Сохраняем стабильность рандомизации при помощи
process - При чем здесь UVM
- Запуск примеров
- Связанная литература
- UVM и конфигурация массива агентов
- To be continued...
О запуске примеров
Для запуска примеров используется Siemens QuestaSim и, где это возможно, Verilator. Используемая версия UVM: 1.2.
Вступление
Доброго времени суток, дорогие читатели! Данная статья является второй в целом цикле, который называется SystemVerilog Gotchas, Tips and Tricks. Первая статья доступна по ссылке. В ней можете узнать, например, о тонкостях disable fork и методах $past() и $sampled().
О мотивации автора к созданию данного цикла статей вы можете также прочитать в первой из них, а мы не будем долго задерживаться на вступлении и познакомимся с новой порцией интересных нюансов в любимом нами языке описания и верификации аппаратуры. Без лишних слов, давайте разбираться!
SystemVerilog process
Встроенный класс process
В SystemVerilog присутствует возможность контролировать отдельные процессы и получать информацию об их состоянии. Реализуется данный функционал при помощи встроенного класса process. Его прототип в стандарте SystemVerilog IEEE Std 1800-2023 представлен в разделе 9.7.
class :final process;
typedef enum {FINISHED, RUNNING, WAITING, SUSPENDED, KILLED} state;
static function process self();
function state status();
function void kill();
task await();
function void suspend();
function void resume();
function void srandom(int seed);
function string get_randstate();
function void set_randstate(string state);
endclass
Подготовка
Создадим пример. Объявим класс seq, выводящий при помощи метода start() информацию о времени симуляции:
module sv_process;
timeunit 1ns;
timeprecision 100ps;
class seq;
string name;
function new(string name);
this.name = name;
endfunction
virtual task start(int delay);
$display("%%5.1f %s started!", $realtime(), name);
for(int i = 0; i < delay; i++) begin
#1ns; $display("%%5.1f %s %0d tick!", $realtime(), name, i);
end
$display("%%5.1f %s ended!", $realtime(), name);
endtask
endclass
...
endmodule
Далее создадим два объекта типа seq и запустим отсчет совместно при помощи fork-join_none:
module sv_process;
...
process seq_0_p;
process seq_1_p;
seq seq_0;
seq seq_1;
fork
begin
seq_0_p = process::self();
seq_0 = new("seq_0");
seq_0.start(10);
end
begin
seq_1_p = process::self();
seq_1 = new("seq_1");
seq_1.start(5);
end
join_none
...
endmodule
Для seq_0 отсчет должен длиться 10ns, для seq_1 5ns. Обратите внимание на переменные типа process, которым мы присваиваем значение process::self(). Теперь при помощи переменных seq_0_p и seq_1_p мы можем управлять процессами, в которых запущены отсчеты вызовами методов seq_0.start(10) и seq_0.start(5) соответственно. Так давайте же займемся этим! Будем добавлять в наш пример новые взаимодействия с переменными процессов.
Управляем процессами
module sv_process;
...
fork
begin
seq_0_p = process::self();
seq_0 = new("seq_0");
seq_0.start(10);
end
begin
seq_1_p = process::self();
seq_1 = new("seq_1");
seq_1.start(5);
end
join_none
#0.5ns;
$display("%5.1f seq_0 status: %p", $realtime(), seq_0_p.status());
$display("%5.1f seq_1 status: %p", $realtime(), seq_1_p.status());
...
endmodule
Ждем 0.5ns и выводим статусы. Результатом будет:
# 0.0 seq_0 started!
# 0.0 seq_1 started!
# 0.5 seq_0 status: WAITING
# 0.5 seq_1 status: WAITING
Метод start() в обоих объектах был выполнен. А что насчет статуса?
Согласно стандарту SystemVerilog IEEE Std 1800-2023:
— WAITING means the process is waiting in a blocking statement.
Процессы действительно ожидают в блокирующем выражении. Это выражение – #1ns в первой итерации метода start(). Оно начало выполняться в нулевой момент времени симуляции и завершиться в момент времени 1ns.
Состояние симуляции в момент 0.5ns схематично представлено ниже.

Действуем далее.
module sv_process;
...
fork
begin
seq_0_p = process::self();
seq_0 = new("seq_0");
seq_0.start(10);
end
begin
seq_1_p = process::self();
seq_1 = new("seq_1");
seq_1.start(5);
end
join_none
#0.5ns;
$display("%5.1f seq_0 status: %p", $realtime(), seq_0_p.status());
$display("%5.1f seq_1 status: %p", $realtime(), seq_1_p.status());
seq_0_p.suspend();
seq_1_p.kill();
$display("%5.1f seq_0 status: %p", $realtime(), seq_0_p.status());
$display("%5.1f seq_1 status: %p", $realtime(), seq_1_p.status());
...
endmodule
Останавливаем процесс для seq_0 при помощи seq_0_p.suspend(), а также принудительно завершаем процесс для seq_1 при помощи seq_1_p.kill(). Выводим статус. Результатом будет:
...
# 0.5 seq_0 status: SUSPENDED
# 0.5 seq_1 status: KILLED
Процесс для seq_0 был остановлен и может продолжить свое выполнение при вызове resume().
Согласно стандарту SystemVerilog IEEE Std 1800-2023:
— SUSPENDED means the process is stopped awaiting a resume.
Процесс для seq_1 был принудительно завершен и не может продолжить свое выполнение. Вызов kill() для процесса также принудительно завершает порожденные им процессы.
Подтвердим два предложения выше выдержками из SystemVerilog IEEE Std 1800-2023:
Calling
resume()on a process that is not in the SUSPENDED state shall have no effect.
The
kill()function forcibly terminates the given process and all its descendant subprocesses, ...
Состояние симуляции в момент 0.5ns схематично представлено ниже.

Продолжим взаимодействовать с процессом для seq_0.
module sv_process;
...
fork
begin
seq_0_p = process::self();
seq_0 = new("seq_0");
seq_0.start(10);
end
begin
seq_1_p = process::self();
seq_1 = new("seq_1");
seq_1.start(5);
end
join_none
#0.5ns;
$display("%5.1f seq_0 status: %p", $realtime(), seq_0_p.status());
$display("%5.1f seq_1 status: %p", $realtime(), seq_1_p.status());
seq_0_p.suspend();
seq_1_p.kill();
$display("%5.1f seq_0 status: %p", $realtime(), seq_0_p.status());
$display("%5.1f seq_1 status: %p", $realtime(), seq_1_p.status());
#10ns;
seq_0_p.resume();
seq_0_p.await();
$display("%5.1f seq_0 status: %p", $realtime(), seq_0_p.status());
$finish();
endmodule
Подождем 10ns, возобновим процесс впри помощи seq_0_p.resume(), подождем его завершения при помощи await() и выведем статус.
Полный результат запуска симуляции представлен ниже:
# 0.0 seq_0 started!
# 0.0 seq_1 started!
# 0.5 seq_0 status: WAITING
# 0.5 seq_1 status: WAITING
# 0.5 seq_0 status: SUSPENDED
# 0.5 seq_1 status: KILLED
# 10.5 seq_0 0 tick!
# 11.5 seq_0 1 tick!
# 12.5 seq_0 2 tick!
# 13.5 seq_0 3 tick!
# 14.5 seq_0 4 tick!
# 15.5 seq_0 5 tick!
# 16.5 seq_0 6 tick!
# 17.5 seq_0 7 tick!
# 18.5 seq_0 8 tick!
# 19.5 seq_0 9 tick!
# 19.5 seq_0 ended!
# 19.5 seq_0 status: FINISHED
Давайте проанализируем, что произошло после выводов SUSPENDED и KILLED.
Процесс, где запущен метод start() класса seq_1, не выполняется (нет соответствующего вывода). Конечно, мы же принудительно завершили его при помощи kill() в момент времени 0.5ns. Теперь об этом процессе можно забыть.
В момент времени 0.5ns (ожидание перед suspend()) + 10ns (ожидание после suspend()) = 10.5ns процесс seq_0 разблокируется и продолжит свое выполнение. При этом он не ждет оставшиеся 0.5ns из задержки #1ns, на которой был прерван, а сразу выводит информацию о времени, после чего итерации продолжаются с обозначенными задержками.
Почему процесс не ждет оставшиеся 0.5ns
Подробнее рассмотрим работу с процессом seq_0. Мы остановили процесс в момент времени 0.5ns. Во время ожидания процессом #1ns. Это интересный случай, когда процесс останавливается в состоянии WAITING. Давайте обратимся к стандарту.
Согласно SystemVerilog IEEE Std 1800-2023:
Suspending a process in the WAITING state shall cause the process to be desensitized to the event expression, wait condition, or delay expiration on which it is blocked.
В контексте стандарта данное предложение означает, что в случае вызова suspend() в состоянии WAITING процесс теряет чувствительность (desensitize) к блокирующему выражению, которого ожидает. Проще говоря, процесс перестает быть заблокированным. Но что же будет, когда после 10ns ожидания мы возобновим процесс? Давайте еще раз обратимся к стандарту.
Согласно SystemVerilog IEEE Std 1800-2023:
Calling resume() on a process that was suspended while in the WAITING state shall resensitize the process to the event expression or to wait for the wait condition to become true or for the delay to expire.
В контексте стандарта данное предложение означает, что если процесс был остановлен при помощи suspend() в состоянии WAITING, то случае вызова resume() этот процесс должен восстановить чувствительность (resensitize) к блокирующему выражению, которого ожидал до suspend(). В нашем случае процесс должен продолжить ожидать завершения задержки.
За те 10ns, что процесс был остановлен, оставшаяся задержка в 0.5ns, очевидно, завершилась. Следовательно, после блокирования из-за resume(), процесс сразу разблокируется, так как задержка, блокировавшая его, уже истекла.
Интересно, что, если бы ожидание после suspend() было равно 300ps, то был бы получен следующий вывод:
# 0.0 seq_0 started!
# 0.0 seq_1 started!
# 0.5 seq_0 status: WAITING
# 0.5 seq_1 status: WAITING
# 0.5 seq_0 status: SUSPENDED
# 0.5 seq_1 status: KILLED
# 1.0 seq_0 0 tick!
# 2.0 seq_0 1 tick!
# 3.0 seq_0 2 tick!
# 4.0 seq_0 3 tick!
# 5.0 seq_0 4 tick!
# 6.0 seq_0 5 tick!
# 7.0 seq_0 6 tick!
# 8.0 seq_0 7 tick!
# 9.0 seq_0 8 tick!
# 10.0 seq_0 9 tick!
# 10.0 seq_0 ended!
# 10.0 seq_0 status: FINISHED
Как думаете, почему?
После завершения метода помощи await(), вывод статуса процесса подтверждает нам его успешное завершение (непринудительное).
Согласно стандарту SystemVerilog IEEE Std 1800-2023:
— FINISHED means the process terminated normally.
Состояние симуляции в момент 19.5ns схематично представлено ниже.

Что же по поводу остальных методов класса process? Их мы косвенно будем использовать в следущей теме!
Запуск примеров
Пример с комментариями: src/test/sv_process.sv.
QuestaSim
Запуск примера для задержки после suspend() в 10ns:
./run_questa.sh sv_process +delay=10000 -c
Запуск примера для задержки после suspend() в 300ps:
./run_questa.sh sv_process +delay=300 -c
Verilator
На момент написания статьи Verilator (v5.040) не поддерживает метод suspend() класса process.
SystemVerilog и стабильность рандомизации
Контекст
Для понимания темы необходимо освоить предыдущую: SystemVerilog process.
В данной теме в том числе будут использованы методы встроенного в SystemVerilog класса process.
Воспроизводимость тестов
В текущем разделе и во всех разделах ниже под словом "случайный" подразумевается "псевдослучайный".
В ходе функциональной верификации зачастую запускаются массивы тестов (регрессии) с различными параметрами. В каждом из тестов генерируются случайные воздействия, которые могут привести к обнаружению ошибки.
Представим, что конкретный тест привел к обнаружению ошибки. В тесте генерировались случайные воздействия. Верификатор должен иметь возможность их воспроизвести. Воспроизводимость случайных воздействий между запусками симуляции называется стабильностью рандомизации.
Стабильности рандомизации посвящен раздел 18.14 стандарта SystemVerilog IEEE Std 1800-2023.
Генераторы случайных чисел (RNG) и их инициализация
Согласно стандарту SystemVerilog IEEE Std 1800-2023:
Each module instance, interface instance, program instance, and package has an initialization RNG. Each initialization RNG is seeded with the default seed. The default seed is an implementation-dependent value.
Тут все понятно. Каждый модуль, интерефейс, программа и пакет имеет генератор случайных чисел (Radnom Number Generator, RNG), состояние которого инициализируется начальным зерном рандомизации симулятора. Это зерно имеет некоторое значение по умолчанию, которое зависит от конкретного симулятора. В QuestaSim вместо значения по умолчанию можно задать пользовательское при помощи аргумента командной строки -sv_seed. Этот аргумент мы еще освоим на практике, но позже.
Процесс инициализации схематично представлен ниже.

Согласно стандарту SystemVerilog IEEE Std 1800-2023:
Each thread has an independent RNG for all randomization system calls invoked from that thread. When a new dynamic thread is created, its RNG is seeded with the next random value from its parent thread. This property is called hierarchical seeding. When a static process is created, its RNG is seeded with the next value from the initialization RNG of the module instance, interface instance, program instance, or package containing the thread declaration.
Здесь тоже разобраться не сложно. Когда создается новый статический поток (при помощи initial, always и always_*), его генератор случайных чисел инициализируется следующим случайным числом из генератора интерфейса, программы или пакета, где этот поток объявлен. Если в статическом процессе создается динамический процесс (при помощи fork), то его генератор инициализируется случайным числом генератора этого статического процесса.
Дополненный процесс инициализации схематично представлен на изображении ниже. Раскрыт только module, для интерфейса, программы и пакета ситуация аналогичная.

Согласно стандарту SystemVerilog IEEE Std 1800-2023:
Each class instance (object) has an independent RNG for all randomization methods in the class. When an object is created using new, its RNG is seeded with the next random value from the thread that creates the object.
Ситуация похожа на ситуацию с потоками. Когда создается новый объект при помощи new(), его генератор случайных чисел инициализируется случайным числом из генератора потока, где этот объект был создан.
Дополненный процесс инициализации схематично представлен на изображении ниже. Добавлено создание объектов.

Стабильность рандомизации объектов
В стандарте явно указаны требования к сохранению стабильности рандомизации объектов.
Object stability shall be preserved when object and thread creation and random number generation are done in the same order as before.
Для сохранения стабильности необходимо сохранение очередности создания объектов. Создание новых объектов необходимо добавлять в конец кода блоков статических и данамических потоков.
Создадим простейший пример с генерацией 4 случайных чисел:
module sv_rand_stability;
initial begin
$display("%8h", $urandom());
$display("%8h", $urandom());
$display("%8h", $urandom());
$display("%8h", $urandom());
end
endmodule
Запустим симуляцию с конкретным начальным зерном рандомизации:
./run_questa.sh sv_rand_stability "-sv_seed 12345"
Результат запуска симуляции:
...
# bedc4ae3
# e6d8f5c9
# e34f5f73
# 680855e0
...
Можно повторить запуск несколько раз и убедиться, что от запуска к запуску результат меняться не будет, так как начальное зерно не изменяется, то есть генератор случайных чисел статического процесса (созданного при помощи initial) инициализируется одним и тем же значением.
Для вывода информации о состоянии генератора случайных чисел можно использовать встроенный в SystemVerilog класс process, разобранный в предыдущей теме, и его метод get_randstate(). В стандарте это явно указано:
The get_randstate() method retrieves the current internal state of an object’s RNG (see 18.14 and 18.15).
Добавим вывод состояния генератора:
module sv_rand_stability;
initial begin
process p;
p = process::self();
$display("RNG: %s", p.get_randstate());
$display("%8h", $urandom());
$display("RNG: %s", p.get_randstate());
$display("%8h", $urandom());
$display("%8h", $urandom());
$display("%8h", $urandom());
end
endmodule
Результат запуска симуляции:
...
# RNG: QS7fc6063ae2c8c21b8cae18c644b6c778
# bedc4ae3
# RNG: QS7fc6063ae2c8c21b87ae6edd5cb65a33
# e6d8f5c9
# e34f5f73
# 680855e0
...
Как можно заметить, после рандомизации состояние генератора потока меняется.
Давайте попробуем нарушить стабильность рандомизации. Вспомним выдержку из стандарта:
When an object is created using new, its RNG is seeded with the next random value from the thread that creates the object.
Значит, при создании объекта RNG поток должен сгенерировать случайное число и проинициализировать им RNG объекта. Давайте создадим объект между рандомизациями чисел в initial. Это неизбежно приведет к изменению состояния RNG в initial, что в свою очередь изменит рандомизацию в нем.
module sv_rand_stability;
class dummy;
rand int data;
endclass
initial begin
dummy d;
$display("%8h", $urandom());
$display("%8h", $urandom());
d = new();
$display("Created dummy!");
$display("%8h", $urandom());
$display("%8h", $urandom());
end
endmodule
Результат запуска симуляции:
...
# bedc4ae3
# e6d8f5c9
# Created dummy!
# 680855e0
# 918c7310
...
Заметим, что число e34f5f73 пропало из вывода. Обратим внимание, что срандомизированное число, сгенерированное после создания объекта, имеет значение 680855e0. Это значение совпадает со значением, идущим после e34f5f73. Видимо, для инициализации RNG объекта d поток initial использовал как раз e34f5f73, выполнив "скрытый" вызов $urandom(). Таким образом, значение e34f5f73 было "выколото" из очереди срандомизированных чисел.
Симуляторы тоже могут ошибаться
Давайте теперь закомментируем модификатор rand в определении класса dummy. Теперь класс не содержит случайных полей, которые могут быть срандомизированы при помощи встроенного метода класса randomize().
module sv_rand_stability;
class dummy;
/* rand */ int data;
endclass
initial begin
dummy d;
$display("%8h", $urandom());
$display("%8h", $urandom());
d = new();
$display("Created dummy!");
$display("%8h", $urandom());
$display("%8h", $urandom());
end
endmodule
Результат запуска симуляции:
...
# bedc4ae3
# e6d8f5c9
# Created dummy!
# e34f5f73
# 680855e0
...
Результат отличается от результата из предыдущего раздела! Можно предположить, что симулятор таким образом оптимизирует свою работу, не инициализируя RNG объектов, в которых нет случайных полей. Причем, такое поведение наблюдается в каждом из трех симуляторов: Siemens QuestaSim, Cadence Xcelium и Synopsys VCS.
Такое поведение симуляторов не соответствует стандарту языка, в котором указано, что каждый объект содержит RNG, который инициализируется при создании этого объекта. В примере выше инициализируется только RNG объектов со случайными полями.
Автор задал вопрос на Stack Oveflow и получил ответ от Dave Rich из Siemens EDA, человека широко известного в узких кругах.
Цитата из ответа:
This behavior is not in compliance with the LRM. This is a frequent occurrence in our industry. Customer's don’t care about the LRM; they just want simulation results to be consistent across vendors. When one vendor has a bug or "optimization" in their tool, other vendors change their tool’s behavior to match the bug or their interpretation to secure business. I've witnessed these requests many times.
И что интересно:
We have a growing list of "undocumented" behaviors that are now diverging from what the LRM specifies for just this one little optimization.
Такая вот большая тройка! Не будем долго заострять на этом внимание, лучше еще поговорим о потоках.
Стабильность рандомизации потоков
В стандарте явно указаны требования к сохранению стабильности рандомизации потоков.
... thread stability can be achieved as long as thread creation and random number generation are done in the same order as before. When adding new threads to an existing test, they can be added at the end of a code block in order to maintain random number stability of previously created work.
Для сохранения стабильности необходимо сохранение очередности создания потоков. Нельзя менять местами initial, always и always_* для статических потоков и fork для динамических. Новые статические потоки необходимо добавлять в конец кода модулей, а новые динамические потоки в конец кода блоков статических потоков.
Вспомним пример с генерацией 4 случайных чисел:
module sv_rand_stability;
initial begin
$display("%8h", $urandom());
$display("%8h", $urandom());
$display("%8h", $urandom());
$display("%8h", $urandom());
end
endmodule
Попробуем нарушить стабильность. Вспомним выдержку из стандарта:
When a new dynamic thread is created, its RNG is seeded with the next random value from its parent thread.
Значит, RNG потока должен сгенерировать случайное число и проинициализировать им RNG созданного в нем потока. Давайте создадим поток динамический при помощи fork. Сделаем это между рандомизациями чисел в initial. Это неизбежно приведет к изменению состояния RNG в initial, что в свою очередь изменит рандомизацию в нем.
module sv_rand_stability;
initial begin
$display("%8h", $urandom());
$display("%8h", $urandom());
fork begin
$display("Created thread!");
$urandom();
end join
$display("%8h", $urandom());
$display("%8h", $urandom());
end
endmodule
Результат запуска симуляции:
...
# bedc4ae3
# e6d8f5c9
# Created thread!
# 680855e0
# 918c7310
...
Заметим, что как и в примере с созданием объекта, число e34f5f73 пропало из вывода. И точно так же для инициализации RNG нового потока родительский поток initial использовал e34f5f73, выполнив "скрытый" вызов $urandom(). Значение e34f5f73 было "выколото" из очереди срандомизированных чисел.
Симуляторы вновь ошибаются
Если не вызывать $urandom() в динамическом потоке, то симулятор не будет инициализировать его RNG, что приведет к повторению результатов рандомизации без создания потока.
module sv_rand_stability;
initial begin
$display("%8h", $urandom());
$display("%8h", $urandom());
fork begin
$display("Created thread!");
end join
$display("%8h", $urandom());
$display("%8h", $urandom());
end
endmodule
Результат запуска симуляции:
...
# bedc4ae3
# e6d8f5c9
# Created thread!
# e34f5f73
# 680855e0
...
Данная оптимизация вносит несоответствие стандарту языка и уже была разобрана на примере с созданием объекта.
Сохраняем стабильность рандомизации при помощи process
Давайте представим ситуацию, что в рамках потока мы не можем точно сказать, будет создан объект или нет. Например, создание будет определяться из командной строки. Минимальный пример представлен ниже.
module sv_rand_stability;
class dummy;
rand int data;
endclass
initial begin
dummy d;
$display("%8h", $urandom());
$display("%8h", $urandom());
if($test$plusargs("create_dummy")) begin
$display("Created dummy!");
d = new();
end
$display("%8h", $urandom());
$display("%8h", $urandom());
$finish();
end
endmodule
Мы хотим при любом раскладе (и с созданием объекта, и без) получать один и тот же набор случайных значений в $urandom(). На помощь нам приходит встроенный класс process, разобранный в предыдущей теме, и его методы set_randstate() и get_randstate().
Как уже было сказано в разделе выше, метод get_randstate() позволяет получить состояние RNG потока. Метод set_randstate() позволяет установить определенное состояние RNG потока. Сохранить стабильность рандомизации для примера выше можно при помощи получения состояния RNG до предполагаемого создания объекта и его восстановления после предполагаемого создания.
module sv_rand_stability;
class dummy;
rand int data;
endclass
initial begin
dummy d; process p; string s;
p = process::self();
s = p.get_randstate();
$display("%8h", $urandom());
$display("%8h", $urandom());
if($test$plusargs("create_dummy")) begin
$display("Created dummy!");
d = new();
end
p.set_randstate(s);
$display("%8h", $urandom());
$display("%8h", $urandom());
$finish();
end
endmodule
Запустим пример с созданием объекта:
./run_questa.sh sv_rand_stability "-sv_seed 12345 +create_dummy +save_randstate"
Запустим пример без создания объекта:
./run_questa.sh sv_rand_stability "-sv_seed 12345 +save_randstate"
Убедитесь, что в обоих случаях рандомизируются одинаковые значения.
При чем здесь UVM
Сохранение стабильности через *_randstate() используется в исходном коде UVM. Если проанализировать файлы, в которых используется данный функционал, то можно сделать вывод, что *_randstate() используется в участках кода, на создание объектов в которых может влиять пользователь. А именно, в коде, относящемся к логированию и базе ресурсов.
Пример из файла uvm_report_catcher.svh:
virtual class uvm_report_catcher extends uvm_callback;
...
local static uvm_report_message m_orig_report_message;
...
static function int process_all_report_catchers(uvm_report_message rm);
...
catcher = uvm_report_cb::get_first(iter,l_report_object);
if (catcher != null) begin
if(m_debug_flags & DO_NOT_MODIFY) begin
process p = process::self(); // Keep random stability
string randstate;
if (p != null)
randstate = p.get_randstate();
$cast(m_orig_report_message, rm.clone()); // ...
if (p != null)
p.set_randstate(randstate);
end
end
...
endfunction
...
endclass
В зависимости от значения поля m_debug_flags класса uvm_report_catcher, может быть выполнено или не выполнено клонирование объекта rm. Метод clone() в UVM создает новый объект. Стабильность рандомизации нарушится, если clone() будет выполнен. Для сохранения стабильности используется обертка из get_randsate() и set_randstate(), которая была разобрана в предыдущем разделе.
Запуск примеров
Пример с комментариями: src/test/sv_rand_stability.sv.
QuestaSim
Рандомизация:
./run_questa.sh sv_rand_stability "-sv_seed 12345"
Рандомизация + создание объекта:
./run_questa.sh sv_rand_stability "-sv_seed 12345 +create_dummy"
Рандомизация + создание потока:
./run_questa.sh sv_rand_stability "-sv_seed 12345 +create_thread"
Рандомизация + создание объекта + *_randstate:
./run_questa.sh sv_rand_stability "-sv_seed 12345 +create_dummy +save_randstate"
Рандомизация + создание потока + *_randstate:
./run_questa.sh sv_rand_stability "-sv_seed 12345 +create_thread +save_randstate"
Verilator
На момент написания статьи Verilator (v5.040) изменял состояние RNG потока при вызове get_randstate(). Поведение не соответствет стандарту. Автор создал issue. Изменения были внесены в исходный код и уже находятся в ветке master. Коммит 94525ca.
Тем не менее, на момент написания статьи в Verilator (v5.040) процессы не имеют собственных RNG, а обращаются к общему. Также автору до конца не понятны механизмы инициализации RNG объектов. Читатель может самостоятельно запустить примеры и убедиться в несоответствиях между Verilator и QuestaSim.
Связанная литература
UVM и конфигурация массива агентов
Предыстория
В предыдущей статье цикла был разобран особый случай совместного запуска последовательностей на множестве агентов. Вспомним, что для корректного совместного запуска последовательностей в fork-join_none, необходима промежуточная переменная (в примере ниже это переменная j).
foreach(seq[i]) begin
fork
int j = i;
seq[j].start(ag[j].sqr);
join_none
end
wait fork;
Это помогает избежать ошибки доступа к памяти. Хорошо, с этим разобрались.
Однако начинающий инженер может достаточно быстро столкнуться с иной проблемой: конфигурацией массива агентов. Как прравило, каждый агент содержит драйвер, который взаимодействует с дизайном при помощи виртуального интерфейса. Но что, если количество подключаемых к устройству интерфейсов параметризуется, а интерфейсы при этом однотипные? Как в случае с интерконнектом. Типовая структурная схема такого верификационного окружения представлена на изображении ниже.

Давайте разберем этот случай и научимся конфигурировать драйверов в агентах каждого своим интерфейсом!
Универсальный интерфейс и драйвер
К интерконнекту может подключаться множество устройств. Интерфейс подключения для каждого устройства, как правило, одинаковый. Упрощенный пример такого интерфейса представлен ниже. Состоит из сигнала valid, сингнализирующего об актуальности адреса и данных, а также из сигналов addr и data соответственно.
interface icon_intf(input logic clk);
logic valid;
logic [ 7:0] addr;
logic [31:0] data;
endinterface
К интерконнекту подключаются агенты, каждый из которых содердит драйвер (в случае активного агента). Драйвер, имитирующий работу устройства, содержит указатель на интерфейс vif, который может получать, например, из базы ресурсов UVM. Пример представлен ниже.
class icon_driver extends uvm_driver#(icon_seq_item);
`uvm_component_utils(icon_driver)
virtual icon_intf vif;
...
virtual function void build_phase(uvm_phase phase);
if(!uvm_resource_db#(virtual icon_intf)::read_by_name(
get_full_name(), "vif", vif))
begin
`uvm_fatal(get_full_name(), "Can't get 'vif'!");
end
endfunction
virtual task main_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
`uvm_info(get_name(), "Got item!", UVM_MEDIUM);
drive();
`uvm_info(get_name(), "Drived Item!", UVM_MEDIUM);
seq_item_port.item_done();
end
endtask
virtual task drive();
repeat($urandom_range(0, 10)) begin
@(posedge vif.clk);
end
vif.valid <= 1'b1;
vif.addr <= req.addr;
vif.data <= req.data;
@(posedge vif.clk);
vif.valid <= 1'b0;
endtask
endclass
В примере взаимодействие с интерфейсом происходит в методе drive(): ожидание случайного количества тактов и простейшее выставление сигнала valid в активный уровень вместе с полезной нагрузкой в виде адреса и данных, полученных из транзакции.
Стоит обратить внимание, что драйвер получает интерфейс из базы ресурсов, опираясь на полное иерархическое имя get_full_name() и название интерфейса в базе ресурсов vif. Это позволяет создавать множество классов типа icon_driver с разными иерархическими именами, и каждый при этом конфигурировать собственным интерфейсом.
Работаем с базой ресурсов
Произведем создание агентов (в каждом из них будет свой драйвер) в классе теста. Для каждого из драйверов отправим в базу ресурсов интерфейс, опираясь на уникальное иерархическое имя. Пример такого класса теста представлен ниже.
class icon_test extends uvm_test;
`uvm_component_utils(icon_test)
int ag_num;
icon_agent ag [];
virtual icon_intf vif [];
...
virtual function void build_phase(uvm_phase phase);
// Получение количества агентов.
void'(uvm_resource_db#(int)::read_by_name(
get_full_name(), "ag_num", ag_num));
// Создание массивов агентов и виртуальных интерфейсов.
ag = new[ag_num];
vif = new[ag_num];
// Создание агентов.
foreach(ag[i]) begin
ag[i] = icon_agent::type_id::create($sformatf("ag_%0d", i), this);
end
// Получение интерфейсов из базы ресурсов.
foreach(ag[i]) begin
void'(uvm_resource_db#(virtual icon_intf)::read_by_name(
get_full_name(), $sformatf("vif_%0d", i), vif[i]));
end
// Отправка интерфейсов в базу ресурсов для каждого из драйверов.
foreach(vif[i]) begin
uvm_resource_db#(virtual icon_intf)::set(
{get_full_name(), $sformatf(".ag_%0d.drv", i)}, "vif", vif[i]);
end
endfunction
...
endclass
В классе объявлены динамические массивы агентов ag и виртуальных интерфейсов vif. Их размер задается переменной ag_num, получаемой из базы ресурсов. После инициализации агентов через type_id::create() происходит получение интерфейсов из базы ресурсов и их повторная отправка в базу ресурсов. Остановимся на взаимодействии с базой чуть подробнее.
В тест интерфейсы попадают из базы ресурсов. Доступ производится по полному иерархическому имени (uvm_test_top) и имени интерфейса с индексом. Раскроем цикл для 3 агентов (в комментариях приведены полные ключи для базы ресурсов):
void'(uvm_resource_db#(virtual icon_intf)::read_by_name(
get_full_name(), "vif_0", vif[i])); // uvm_test_top.vif_0
void'(uvm_resource_db#(virtual icon_intf)::read_by_name(
get_full_name(), "vif_1", vif[i])); // uvm_test_top.vif_1
void'(uvm_resource_db#(virtual icon_intf)::read_by_name(
get_full_name(), "vif_1", vif[i])); // uvm_test_top.vif_2
Отправка в базу ресурсов для конкретных драйверов будет осуществляться по конкатенации полного иерархического имени теста (uvm_test_top), иерархического пути до агента с индексом и имени интерфейса vif. Раскроем цикл для 3 агентов (в комментариях приведены полные ключи для базы ресурсов):
uvm_resource_db#(virtual icon_intf)::set(
{get_full_name(), ".ag_0.drv", i}, "vif", vif[i]); // uvm_test_top.ag_0.drv.vif
uvm_resource_db#(virtual icon_intf)::set(
{get_full_name(), ".ag_1.drv", i}, "vif", vif[i]); // uvm_test_top.ag_1.drv.vif
uvm_resource_db#(virtual icon_intf)::set(
{get_full_name(), ".ag_2.drv", i}, "vif", vif[i]); // uvm_test_top.ag_2.drv.vif
Заметим, что выше агенты создаются при помощи:
foreach(ag[i]) begin
ag[i] = icon_agent::type_id::create($sformatf("ag_%0d", i), this);
end
Если мысленно раскрыть цикл, то для примера из 3 агентов их полными иерархическими именами будут uvm_test_top.ag_0, uvm_test_top.ag_1 и uvm_test_top.ag_2.
С базой ресурсов разобрались. Теперь наше окружение поддерживет динамическое количество агентов и отправку соответствующих интерфейсов в драйверы. Осталось только отправить интерфейсы в класс теста из топ-модуля. Но и тут не обойдется без нюансов. Смотрим!
Отправка массива интерфейсов в окружение
Пример простейшего топ-модуля представлен ниже.
module fork_join_seqs;
...
parameter AG_NUM = 8;
logic clk;
...
icon_intf intf [AG_NUM] (clk);
generate
for(genvar i = 0; i < AG_NUM; i++) begin
initial begin
uvm_resource_db#(virtual icon_intf)::set(
"uvm_test_top", $sformatf("vif_%0d", i), intf[i]);
end
end
endgenerate
initial begin
uvm_resource_db#(int)::set("uvm_test_top", "ag_num", AG_NUM);
run_test("icon_test");
end
endmodule
Количество агентов определяется параметром AG_NUM. Этот параметр определяет размер массива статических интерфейсов vif. Здесь мы сталкиваемся с ограничениями статических интерфейсов. Размер массива интерфейсов должен быть известен на этапе элаборации, а сам массив не может быть создан динамически в ходе симуляции.
Обратите внимание, что данное правило не распространяется виртуальные интерфейсы, которые являются не более, чем указателями на статические. В разделе выше мы свободно инициализировали динамический массив виртуальных интерфейсов.
Обратите внимание на конструкцию generate. При помощи нее мы создаем независимые initial-блоки, в которых отправляем в базу ресурсов указатели на интерфейсы с соответствующим индексом. Если мысленно раскрыть цикл, то ключами в базе ресурсов для интерфесов будут являться uvm_test_top.vif_0, uvm_test_top.vif_1, uvm_test_top.vif_2 и так далее. Именно по этим ключам мы забирали указатели в классе тесты в разделе выше.
Почему для итерирования не подходит процедурный цикл
Вы спросите, почему отправку указателей на интерфейсы нельзя сделать в одном initial-блоке, например следующим образом:
initial begin
for(int i = 0; i < AG_NUM; i++) begin
uvm_resource_db#(virtual icon_intf)::set(
"uvm_test_top", $sformatf("vif_%0d", i), intf[i]);
end
end
При компиляции кода выше в QuestaSim вы получите ошибку:
Error: ./test/fork_join_seqs.sv(...): Illegal index into array of interfaces.
В стандарте SystemVerilog IEEE Std 1800-2023 при выборе элемента из массива экземпляров иерархии (hierarchical instance array) индекс может быть только константным выражением:
Names in a hierarchical path name that refer to instance arrays or loop generate blocks may be followed immediately by a constant expression in square brackets. This expression selects a particular instance of the array and is, therefore, called an instance select. The expression shall evaluate to one of the legal index values of the array.
Итератор же цикла в процедурном блоке является переменной, а не константой.
В цикле, объявленном в generate, итератор должен иметь тип genvar, которому может быть присвоено только константное выражение (см. раздел 27.3 SystemVerilog IEEE Std 1800-2023):
genvar_initialization ::=
[ genvar ] genvar_identifier = constant_expression
Таким образом, цикл в generate подходит для итерирования по массиву экземпляров иерархии.
Далее остается отправить в базу ресурсов количество агентов для корректной инициализации массивов в классе теста.
Временная диаграмма для теста из 4 агентов представлена ниже.

Запуск примеров
Пример с комментариями: src/test/agent_array.sv.
QuestaSim
Запуск примера для одного агента:
./run_questa.sh agent_array -gAG_NUM=1
Для четырех агентов:
./run_questa.sh agent_array -gAG_NUM=4
Verilator
На момент написания статьи Verilator (v5.040) не поддерживает UVM. Текущий статус поддержки можно посмотреть в соответствующем issue.
To be continued...
Вторая часть цикла SystemVerilog Gotchas, Tips and Tricks подошла к концу, дорогие читатели. Следите за обновлениями в Telegram-канале автора Verification For All (VFA)!
Всего вам наилучшего, дорогие читатели! Мира и процветания!☀️