Приветствую тебя, дорогой единомышленник! В данном репозитории содержатся исходные файлы статей канала Telegram-канала Verification For All.
Список статей:
- SystemVerilog и виртуальный интерфейс.
- Верификация на SystemVerilog. "Гонки" сигналов на симуляции.
- Кодовое покрытие в функциональной верификации.
- Обзор SystemVerilog IEEE 1800-2023.
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.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.