Верификация на 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.
Хорошего тебе дня и до новых встреч!