Приветствую тебя, дорогой единомышленник! В данном репозитории содержатся исходные файлы статей канала Telegram-канала Verification For All.

Список статей:

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, потому что согласно логу выше первая ошибка была обнаружена именно в этот момент времени.

Симуляция ошибочного модуля тестирования, время 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. Однако симулятор может выполнять присвоения из этих двух блоков в любой последовательности.

Например:

  1. a = 10;
  2. b = 5;
  3. b = 7;
  4. a = 20;

Или:

  1. b = 5;
  2. a = 10;
  3. a = 20;
  4. 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)) последовательность выполнения не определена.

Она может быть такой (что нас устраивает):

  1. p.a = a;
  2. p.b = b;
  3. p.c = c;
  4. mbx.put(p);
  5. a = $urandom_range(0, 5);
  6. b = $urandom_range(0, 5);

А может быть и такой (что нас не устраивает):

  1. a = $urandom_range(0, 5);
  2. b = $urandom_range(0, 5);
  3. p.a = a;
  4. p.b = b;
  5. p.c = c;
  6. mbx.put(p);

Вернемся к моменту времени в 50ns.

Симуляция ошибочного модуля тестирования, время 50ns

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

Симуляция ошибочного модуля тестирования, время 30ns, Event Mode + Expand All

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

Симуляция ошибочного модуля тестирования, время 30ns, Event Mode + Expand All

Видим значения 30ns + 2 + 4 + .... Это как раз таки очередность изменения сигналов в ходе одного региона выполнения времени 30ns. А теперь внимательно посмотрите, какие данные сохраняются в пакет p. Верно, 0x0 и 0x4. Почему? Потому что сначала выполнилось a = $urandom_range(0,5) (вернуло 0), а затем p.a = a. Получается, что вместо 0x3 и 0x4 в пакет попали 0x0 и 0x4.

  1. a = $urandom_range(0, 5);
  2. p.a = a;
  3. p.b = b;
  4. p.c = c;
  5. mbx.put(p);

Для b = $urandom_range(0, 5) точно сказать нельзя, потому что случайное число этого такта совпало с числом на предыдущем такте и наблюдать изменения мы не можем. Но в данном контексте это не важно.

А что происходит дальше? Передвигаемся в следующий такт и попадаем в момент 50ns.

Симуляция ошибочного модуля тестирования, время 50ns, Event Mode + Expand All

Здесь нам важно только сохранение результата. Обратите внимание, что сохранение результата 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 для ошибочного и справленного модулей тестирования. Сверху ошибочный модуль, снизу - исправленный.

Симуляция времени 30ns для ошибочной (сверху) и исправленной (снизу) симуляции, Event Mode + Expand All

Видим, что в исправленном окружении значение 0x0 подается на вход a после сохранения информации о значениях на входах в текущий момент времени. Происходит это потому, что выставление значения на вход делается через неблокирующее присваивание (<=), а считывание значений через блокирующее.

Здесь мы сами для себя дополнили озвученное выше правило, звучать оно теперь будет так:

При тестировании последовательностного устройства входные воздействия следует подавать через неблокирующие (<=) присваивания, а считывать выходные и проверять их - через блокирующие (=).

Таким образом, в моменте времени 30ns получаем пакет с верными данными 0x3 и 0x4. Далее, в моменте времени 50ns (на следующем такте) получаем данные с выхода.

Симуляция ошибочного модуля тестирования, время 50ns, Event Mode + Expand All

После получения пакета с результатом в этом же такте происходит происходит сравнение: 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
    
    // ...

которые и привели к выполнению соответствующих участков кода.

-> GitHub примера

А если посложнее

Так, ну с базовым примером разобрались, теперь сделаем модуль для тестирования чуть интереснее. Алгоритм тот же, разбираем дизайн, тесты, запускаемся и анализируем результаты.

Подопытный

Можем считать, что это некий декодер, который в зависимости от входа 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.

-> GitHub примера

Немного теории

Типы кодового покрытия

Так, разбирая примеры, мы с вами попутно познакомились с двумя часто используемыми типами кодового покрытия: процедурным покрытием (англ. 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.

-> GitHub примера

Сбор кодового покрытия

Для получения кодового покрытия инженеру не нужно ничего описывать при помощи синтаксиса 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

-> GitHub примера

Но и это еще не все. Также мы можем сформировать детализированный текстовый отчет или 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

-> GitHub примеров

Пример 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

Машина одна и та же.

-> GitHub примеров

Так что при сборе кодового покрытия внимательно настраивайте области, для которых хотите его осуществлять, стараясь минимизировать работу симулятора.

Причины использовать кодовое покрытие

Так уж сложилось, что начинающими инженерами кодовое покрытие собирается и анализируется реже, чем функциональное. Однако, у него есть свои преимущества и цели использования.

Основные преимущества:

  • практически все за вас делает симулятор, нет необходимости писать дополнительный код на 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, то откроется диаграмма переходов, на ней тоже явно можем увидеть количество переходов между состояниями. Примечательно, что симулятор, проанализировав код, сам сделал выводы о том, какие переходы возможны, а какие нет. Красота!

-> GitHub примера

Исправляемся

Допишем тестовый сценарий для инициации нужных переходов:

    // ...

    // 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

Смотрим теперь на покрытие состояний:

Все переходы были совершены.

-> GitHub примера

Исправляем 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

Мы прекрасны!

-> GitHub примера

Заключение

Что ж, дорогой читатель, вот мы и добрались до конца. Было ли интересно? Лично мне - да. Потому что, "стыдно признаться, грех утаить", а я и сам достаточно давно не собирал кодовое покрытие:) Исправлюсь, обещаю.

Надеюсь, прочитав текст выше, ты открыл для себя что-то новое.

Спасибо тебе за уделённое время. Всего наилучшего и до новых встреч!

А больше подобных заметок ты можешь найти в 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.