Найти тему
OVERCLOCKERS.RU

Разработка железа на System Verilog HDL/VHDL с использованием верилатора. Часть 3

В части 1 и части 2 мы обсудили основы использования Verilator и написания тестовых стендов C++ для модулей Verilog/SystemVerilog, а также способы выполнения основных задач проверки: управление входными данными, наблюдение за выходными данными, генерация случайных стимулов и реализация непрерывной проверки, подобной утверждению. В этом руководстве мы рассмотрим написание простого тестового стенда для проверки функциональности сложения и вычитания нашего ALU. Обеспечение доступности typedef operation_t в тестовом стенде C++Прежде чем мы сможем начать проверять, правильно ли наше ALU складывает или вычитает два числа, мы должны сначала сделать нашу операцию typedef доступной для нас в нашем тестовом стенде tb_alu.cpp. В верхней части нашего кода ALU у нас есть следующе

typedef enum logic [1:0] { add = 2'h1, sub = 2'h2, nop = 2'h0} operation_t /*verilator public*/;

Обратите внимание на комментарий /*verilator public*/ после operation_t. Это говорит верилатору преобразовать этот typedef в C++ и сделать его общедоступным — это происходит на этапе проверки (преобразование HDL в C++).Эти типы комментариев называются директивами или прагмами — они дают Verilator дополнительную информацию о том, как обрабатывать ваш HDL-код.Если вы посмотрите на папку obj_dir, где находятся наши артефакты преобразования, вы найдете Valu___024unit.h, который содержит проверенную версию нашего перечисления typedef:

// TYPEDEFS // That were declared public enum operation_t { add = 1U, sub = 2U, nop = 0U };

Если бы мы не добавили комментарий /*verilator public*/, это определение типа не было бы предоставлено в нашем заголовочном файле.Поскольку мы уже включили этот заголовок в наш tb_alu.cpp (#include "Valu___024unit.h"), теперь мы можем использовать определения внутри перечисления для управления вводом op_in ALU.Доступ к значениям перечисления можно получить в нашем тестовом стенде следующим образом:

Valu___024unit::operation_t::addValu___024unit::operation_t::sub

ПРИМЕЧАНИЕ. Если у тестируемого устройства, над которым вы работаете, есть подмодули, и вы используете директиву public внутри любого из подмодулей, вам может потребоваться найти и включить дополнительные файлы заголовков, сгенерированные из этих конкретных подмодулей.На данный момент у нас есть все необходимое, чтобы продолжить и проверить функциональность сложения и вычитания нашего ALU.

Пример традиционной (временной) проверкиВо второй части был продемонстрирован примитивный метод проектирования tesbench, который подтвердил правильность выходных данных нашего ALU.Мы проверили правильность работы конвейера между in_valid и out_valid, применив 1 к in_valid на 5-м такте и проверив, что out_valid равно 1 на 7-м такте:

if (posedge_cnt == 5){ dut->in_valid = 1; // assert in_valid on 5th cc}if (posedge_cnt == 7){ if (dut->out_valid != 1) // check in_valid on 7th cc std::cout << "ERROR!" << std::endl;}

Этот пример сам по себе довольно плохой (что, если значение out_valid постоянно равно 1?), но оно хорошо иллюстрирует суть. Я лично называю этот стиль традиционной или основанной на времени проверкой, потому что мы явно применяем некоторые определенные входные значения в определенное время и проверяем, соответствуют ли выходные значения тому, что мы ожидаем, по прошествии некоторого времени. Этот стиль отлично подходит для быстрой проверки небольших проектов, хотя может быть не лучшим вариантом при работе со сложными модулями.

Стимул, основанный на времениЕсли вы следуете этому руководству после завершения части 2, ваш основной цикл должен теперь выглядеть так:

while (sim_time < MAX_SIM_TIME) { dut_reset(dut, sim_time); dut->clk ^= 1; dut->eval(); if (dut->clk == 1){ dut->in_valid = 0; posedge_cnt++; set_rnd_out_valid(dut, sim_time); check_out_valid(dut, sim_time); } m_trace->dump(sim_time); sim_time++;}

Давайте выполним операцию сложения на 10-м такте и операцию вычитания на 20-м такте. Для этого мы удалим случайную допустимую функцию отправки, которая находится в строке 10 выше, и заменим ее оператором switch следующим образом:

while (sim_time < MAX_SIM_TIME) { dut_reset(dut, sim_time); dut->clk ^= 1; dut->eval(); if (dut->clk == 1){ dut->in_valid = 0; posedge_cnt++; switch (posedge_cnt){ case 10: dut->in_valid = 1; dut->a_in = 5; dut->b_in = 3; dut->op_in = Valu___024unit::operation_t::add; break; case 20: dut->in_valid = 1; dut->a_in = 5; dut->b_in = 3; dut->op_in = Valu___024unit::operation_t::sub; break; } check_out_valid(dut, sim_time); } m_trace->dump(sim_time); sim_time++;}

Как видите, в обоих случаях мы устанавливаем dut->in_valid в 1, устанавливаем некоторые входные операнды в dut->a_in и dut->b_in, а затем устанавливаем dut->op_in на желаемую операцию: сложение на 10-м такте и вычитание на 20-м. При моделировании мы можем увидеть, как это работает:

Рис. 1: Результаты моделирования сложения и вычитания

Из рисунка 1 видно, что АЛУ работает корректно, но на самом деле мы должны делать проверки в коде.

Проверка результатов по времениМы знаем, что конвейеру ALU всегда требуется фиксированное количество 2 тактов для обработки наших входов и возврата результата на выходах, поэтому мы можем добавить проверки на 12-м и 22-м тактах для проверки результатов:

switch (posedge_cnt){ case 10: dut->in_valid = 1; dut->a_in = 5; dut->b_in = 3; dut->op_in = Valu___024unit::operation_t::add; break; case 12: if (dut->out != 8) std::cout << "Addition failed @ " << sim_time << std::endl; break; case 20: dut->in_valid = 1; dut->a_in = 5; dut->b_in = 3; dut->op_in = Valu___024unit::operation_t::sub; break; case 22: if (dut->out != 2) std::cout << "Subtraction failed @ " << sim_time << std::endl; break;}

Нам не нужно явно проверять правильность out_valid в случаях 12 и 22 — это все еще делается нашей функцией check_out_valid().Если мы сейчас запустим симуляцию, то увидим, что она проходит без проблем, потому что АЛУ написан правильно. Однако, если мы зайдем в alu.sv и возьмемся за вектор результата:

//sub: result = a_in_r + (~b_in_r+1'b1); // originalsub: result = a_in_r + (~b_in_r+6'h3); // modified

Мы увидим, что симуляция не удалась и сообщает нам, где была обнаружена проблема:

### SIMULATING ###./obj_dir/Valu +verilator+rand+reset+2Subtraction failed @ 42

ЗаключениеВ этом руководстве демонстрируется пример примитивного кода проверки на C++, который можно использовать для основных задач проверки. Существует много вариантов того, как вы можете структурировать этот тип кода — вы можете использовать операторы switch, как показано здесь, отдельные операторы if или разделить код на отдельные функции. Тем не менее, вы, вероятно, согласитесь с тем, что такого рода методология проверки, хотя и является простой и быстрой в реализации, на самом деле недостаточна для более сложных проектов.

Что дальше?Теперь, когда вы получили хорошее представление о том, как стимулировать и проверять свой проект с помощью Verilator, в части 4 будет показано, как писать рандомизированные транзакционные (в стиле UVM) тестовые стенды на C++.

📃 Читайте далее на сайте