38 KiB
Руководство по поиску и исправлению ошибок в проекте
Цель
При выполнении лабораторных работ вы непременно будете сталкиваться с множеством ошибок. И это нормально: "Не ошибается тот, кто ничего не делает" — © Джейсон Стейтем.
Важно воспитать в себе положительное восприятие обнаружения ошибок (ведь это приводит к улучшению вашего творения). Если относиться к обнаружению ошибок отрицательно, то вы подсознательно будете пытаться найти ошибки спустя рукава, но если вы "в домике", и ошибок не видите — это не значит что их нет.
При должном отношении, поиск ошибок может превратиться в увлекательное детективное расследование, где у вас есть "место преступления" (обнаруженное несоответствие в поведении, обычно это не сама ошибка, а ее следствие, круги на воде) и какой-то "набор улик" (фрагменты лога, исходный код). И вы по чуть-чуть будете разматывать "нераспутываемую паутину лжи", получая все новые улики, ведущие к истинной ошибке.
Этот документ посвящен практикуму по поискам подобных ошибок в SystemVerilog-коде.
- Руководство по поиску и исправлению ошибок в проекте
- Цель
- Алгоритм поиска ошибок
- Работа с логом при появлении ошибок
- Поиск ошибки на временной диаграмме
- Открытие файла исходного кода проблемного сигнала
- Добавление сигналов объектов на временную диаграмму
- Сброс симуляции и ее повтор, установка времени моделирования
- Исправление сигналов с Z-состоянием
- Поиск ошибки в сигналах, формирующих проблемный сигнал
- Исправление логики проблемного сигнала
- Проблема необъявленных сигналов
- Самостоятельная работа
Алгоритм поиска ошибок
- Обычно всё начинается с сообщения в логе тестов (никто не проверяет глазами временную диаграмму сложных проектов, состоящую из тысяч сигналов, меняющихся миллионы раз за микросекунду), но на наших простых лабах, этот шаг иногда может быть и пропущен.
Сообщение в логе обычно содержит следующую ключевую информацию: имя сигнала, на котором установилось неверное значение, и время когда это произошло. Чем лучше написаны тесты, тем больше ключевой информации будет отражено в сообщении, поэтому написание тестов является своего рода искусством. - Получив имя сигнала и время, мы отправляемся на временную диаграмму и проверяем нашу ошибку. Как это сделать? Необходимо определить по коду, какие сигналы и каким образом управляют нашим сигналом. Вариантов может быть несколько:
- Управляющие сигналы имеют корректное значение, но логика, по которой они управляют сигналом неверна, из-за этого на нем возникает неверное значение.
Это идеальный случай, при возникновении которого мы сразу же находим причину проблемы и исправляем ее. - Логика управления верна, а какая-то часть управляющих сигналов имеет неверное значение (пусть для примера, неверное значение будет на управляющем сигнале
X
). Это означает, что обнаруженное несоответствие сигналов является уже следствием какой-то ошибки, и мы должны вернуться к шагу 2, проверяя источники сигналов для сигналаX
. Так происходит до тех пор, пока мы не попадаем в тип 1. - Логика управления и значения управляющих сигналов верны. Это самый сложный тип ошибок, который заключается либо в ошибке в спецификации разрабатываемого устройства, либо в САПРе или компонентах, влияющих на его работу. В рамках данного курса вас не должны заботить данные ошибки, и при их возникновении вам стоит обратиться к преподавателю (предварительно убедившись, что ошибка совершенно точно не подходит под первые два варианта).
- Любая возможная комбинация всех предыдущих типов.
- Управляющие сигналы имеют корректное значение, но логика, по которой они управляют сигналом неверна, из-за этого на нем возникает неверное значение.
- Обнаружив первопричину ошибки, мы исправляем ее (возможно дополняя набор тестов, или внеся правки в спецификацию), и повторно запускаем все тесты, чтобы убедиться в двух вещах:
- ошибка действительно исправлена
- исправление ошибки не породило новых ошибок
Давайте отработаем эти шаги на примере отладки ошибок в проекте по вычислению приблизительной длины вектора.
Работа с логом при появлении ошибок
После запуска симуляции мы видим в логе множество ошибок:
В любой ситуации с множеством ошибок, сначала надо разбираться с самой первой из них, поскольку она может быть ключом к появлению всех остальных. Поэтому листаем лог до момента первой ошибки:
В логе сказано, что в момент времени 5ns
, на дизайн подавались координаты вектора, равные 0
и 0
, модель посчитала, что длина вектора равна нулю, в то время, как дизайн вернул значение x
.
Поиск ошибки на временной диаграмме
Давайте найдем это место на временной диаграмме. Обычно, сразу после запуска симуляции на временной диаграмме отображено место, где симуляция остановилась (возможно с очень неподходящим масштабом). Для начала подгоним масштаб таким образом, чтобы вся временная диаграмма умещалась в окне. Это делается либо нажатием правой кнопкой мыши по в области отображения сигналов, с выбором "Full View" во всплывающем меню, либо нажатием на кнопку Затем найдем приблизительное место рядом с тем временем, что нас интересует, установим там курсор, и приблизим масштаб, периодически уточняя местоположения курсора, пока не найдем интересующее нас место.
Мы видим ровно ту информацию, которую нам предоставил тестбенч. Теперь надо разобраться в причинах возникновения X-состояния. Такое может произойти в двух ситуациях: какой-то из сигналов, формирующих этот находится в X
или Z
состоянии, либо же два каких-то сигнала одновременно пытаются выставить разные значения (подобный вариант встречается куда реже и в цикле ваших лабораторных вряд ли встретится).
Открытие файла исходного кода проблемного сигнала
В любом случае, первым делом необходимо определить, источник формирования значения сигнала res
. Для этого, откроем файл с исходным кодом, где определен данный сигнал. Для этого, нажмем правой кнопкой мыши по имени сигнала на временной диаграмме, и выберем Go To Source Code
:
Открывается следующий код (с курсором на строчке wire [31:0] res;
):
module tb();
reg [31:0] a;
reg [31:0] b;
wire [31:0] res;
vector_abs dut(
.x(a),
.y(b),
.abs(res)
);
//...
Выделив res
мы видим, что у нас подсветился res
в строке abs(res)
, что означает что мы завели наш провод внутрь объекта dut
модуля vector_abs
, и у нас проблема второго типа (X-состояние передалось от выхода abs
модуля vector_abs
проводу res
модуля tb
).
В этом можно убедиться, если вытащить сигналы модуля vector_abs
на временную диаграмму. Чтобы это сделать, надо переключиться на окно Scope
, где размещена иерархия объектов нашего тестбенча
Добавление сигналов объектов на временную диаграмму
Обратите внимание, что в иерархии окна
Scope
находятся не имена модулей, а имена сущностей модуля. В приведенном выше листинге кода мы создали сущность модуляvector_abs
с именемdut
, поэтому в иерархииScope
мы видим внутри модуля верхнего уровня объектdut
(неvector_abs
), так будет и со всеми вложенными объектами.
Выделим объект dut
. В окне Objects
справа отобразятся все внутренние сигналы (входы/выходы, внутренние провода и регистры) объекта dut
:
Вообще говоря, мы уже видим, что выход abs
(к которому подключен наш провод res
) находится в X-состоянии, но для отработки навыков, разберемся с добавлением на временную диаграмму. Можно поступить двумя способами:
- Добавить все сигналы (то что видно в окне
Objects
на временную диаграмму) из окнаScope
для этого, либо перетаскиваем нужный нам объект, зажав левую кнопку мыши на временную диаграмму, либо жмем правой кнопкой мыши по нужному объекту, и выбираемAdd to Wave Window
- Добавить отдельные сигналы из окна
Objects
. Для этого выделяем их (возможно множественное выделение через модификаторыshift
илиctrl
), и как и в прошлом случае, либо перетаскиваем сигналы левой кнопкой мыши, либо добавляем их через правую кнопку мыши.
Обратите внимание, что часть сигналов отображают какое-то значение (сигнал
abs
отображает X-состояние), а часть не отображают ничего. Так произошло, потому что проводabs
непрерывно связан с проводомres
, с точки зрения симулятора это одна сущность, и записывая во время моделирования значения для сигналаres
, симулятор неявно записывал значения для сигналаabs
, чего не скажешь про остальные сигналы, которых не было во время моделирования на временной диаграмме.
Сброс симуляции и ее повтор, установка времени моделирования
Для того, чтобы получить отсутствующие значения, необходимо повторить моделирование. Для этого, необходимо сбросить время моделирования в 0 и запустить его снова.
Для этого, необходимо на панели симуляции нажать кнопку Restart
(|◀
), а затем кнопку Run all
(▶
) или Run for
(▶t
)
Run for
выполняет моделирование указанного количества времени, после чего моделирование приостанавливается. Моделирование может быть остановлено так же и вручную, либо вызовом соответствующей инструкции из кода теста.
Run all
отличается от Run for
тем, что в качестве количества моделируемого времени указывается "бесконечность", и моделирование будет остановлено только вручную, либо вызовом соответствующей инструкции.
Обратите внимание, что для добавления недостающих значений добавленных сигналов лучше всего выполнять описанную выше инструкцию. Аналогичного результата можно добиться и нажатием на кнопку
Relaunch Simulation
, однако эта команда запускает повторную компиляцию и запуск симуляции, что для крупных проектов выльется в потерю времени на излишнюю компиляцию.
Панель управления симуляции с кнопками:
Restart
Run all
Run for
Relaunch Simulation
Кроме того, чтобы курсор и лог снова не ушли далеко от места первой ошибки, можно сразу указать, необходимое нам время моделирования перед выполнением команды Run for
: 5ns
В итоге видим следующую картину на временной диаграмме:
Видим два сигнала в Z-состоянии и один сигнал в X-состоянии. Обычно, сигналы с Z-состоянием проще всего исправить, т.к. зачастую это забытое или некорректное подключение провода. Кроме того, сигнал, зависящий от сигнала с Z-состоянием может оказаться в X-состоянии, так что это может быть решением нашей проблемы, поэтому займемся проводами min
и min_half
. Сперва займемся сигналом min
и перейдем к шагу 2 нашего алгоритма (нажимаем правой кнопкой мыши и выбираем Go To Source Code
):
module vector_abs(
input [31:0] x,
input [31:0] y,
output[31:0] abs
);
wire [31:0] min;
wire [31:0] min_half;
max_min max_min_unit(
.a(x),
.b(y),
.max(max),
.min(min)
);
Исправление сигналов с Z-состоянием
Мы видим, что сигнал min
подключен к выходу min
объекта max_min_unit
модуля max_min
. Добавим сигналы этого модуля на временную диаграмму. Для этого, необходимо раскрыть список объектов, содержащихся в объекте dut
иерархии объектов Scope
и выбрать там объект max_min_unit
:
Добавляем внутренние сигналы на временную диаграмму, и повторяем моделирование:
Произошло что-то странное: все внутренние сигналы объекта max_min_unit
"зеленые" (не имеющие X или Z состояния), однако подключенный к выходу этого модуля сигнал min
находится в Z-состоянии. Как такое могло произойти?
Если присмотреться к сигналу min
, находящемуся в Z-состоянии, можно заметить, что младшая цифра находится не в Z-состоянии, а в состоянии 0
, такое же значение стоит и на сигнале min
объекта max_min_unit
. Это интересно.
Если присмотреться к этим двум сигналам еще пристальней, то можно увидеть, что у сигнала min
объекта dut
разрядность 32 бита, в то время как разрядность сигнала min
объекта max_min_unit
составляет 4 бита.
Это и является проблемой: мы подключили 4 бита сигнала 4-разрядного сигнала min
к младшим 4 битам 32-разрядного сигнала min
, а остальные разряды остались не подключенными.
По всей видимости, при написании модуля max_min
, была указана неверная разрядность сигнала min
, вместо 31
было написано 3
. Исправим это и повторим моделирование.
Обратите внимание, что поскольку мы изменили исходный код, в этот раз необходимо нажать на кнопку
Relaunch Simulation
, поскольку нужна повторная компиляция проекта.
В логе сообщается о 102 найденных ошибках. Это ровно на одну ошибку меньше, чем было ранее. Это не означает, что в проекте осталось 102 ошибки, только то, что исправив данную ошибку, мы действительно что-то исправили, и теперь один из тестовых сценариев, который ранее завершался ошибкой, теперь завершился без нее. Помните, что если в проекте много ошибок, то часть ошибок может выправлять поведение других ошибок (хоть и не всегда, но иногда минус на минус может выдать плюс контексте ошибок проекта), поэтому надо осторожно полагаться на число найденных ошибок, если их больше нуля.
Посмотрим на нашу временную диаграмму снова, и выберем дальнейшие действия:
Мы видим, что на временной диаграмме не осталось сигналов в X или Z-состоянии, а значит мы собрали все "низковисящие" улики нашего с вами расследования. Вернемся к месту преступления и попробуем поискать новые улики:
Поиск ошибки в сигналах, формирующих проблемный сигнал
Мы видим, что первой ошибкой в логе стала не та ошибка, что была прежде. Раньше первый неверный результат мы видели в момент времени 5ns
, когда на дизайн подавались значения 0
и 0
, теперь же первой ошибкой стал момент времени 10ns
, когда на дизайн подаются значения 1
и 1
. Наше устройство считает, что результат должен равняться 3
, в то время как модель считает, что результат должен равняться 1
. Проверим, нет ли ошибки в модели и посчитаем результат самостоятельно:
Для определения приблизительной длины вектора в евклидовом пространстве(вычисления квадратного корня из суммы квадратов / длины гипотенузы прямоугольного треугольника) можно воспользоваться формулой:
sqrt(a^2 + b^2) ≈ max + min/2
, где max
и min
— большее и меньшее из пары чисел соответственно [Ричард Лайонс: Цифровая обработка сигналов, Глава 13.2, стр. 475].
Подставим наши числа в формулу (поскольку оба числа равны, не важно какое из них будет максимумом, а какое минимумом):
1 + 1/2 = 1.5
Ни модель ни дизайн не правы?
На самом деле, наше устройство поддерживает только целочисленную арифметику, поэтому результат будет:
1 + 1/2 = 1 + 0 = 1
Модель правильно отразила особенность нашего устройства и дала корректный результат.
Значит надо смотреть как формируется результат в нашем устройстве, посмотрим на выход abs
в модуле vector_abs
:
assign abs = max + min_half;
Выход abs
зависит от двух внутренних сигналов: max и min_half
. В соответствии с нашим алгоритмом, либо проблема в логике, связывающей эти два сигнала (операции сложения), либо в значении какого-то из этих сигналов, либо комбинации этих вариантов.
Изучив модуль, мы понимаем, что в логике этого присваивания проблем нет, т.к. оно повторяет логику формулы max + min/2
, складывая максимум с половиной минимума. Значит проблема в значении какого-то из этих сигналов (или обоих из них). Посчитаем значения этих сигналов самостоятельно (для сложного проекта эти значения бы посчитала модель):
1
и 0
.
Смотрим, какие значения установлены на сигналах max
и min_half
в момент времени 10ns
:
Мы видим, что в момент времени 10ns
значения max
и min_half
изменились ак 1 -> 4
и 2 -> 8
соответственно. Нас интересуют значения 1
и 2
, т.к. в момент времени 10ns
на выходе дизайна в этот момент был установившийся результат для предыдущих значений (еще не успел посчитаться результат для новых значений).
Значение max=1
совпадает с ожидаемым, в то время как min_half=2
явно нет.
Мы нашли причину неправильного вычисления результата: и правда, 1+2=3
, теперь необходимо найти ошибку в вычислении сигнала min_half
.
Как и с сигналом abs
, необходимо определить сигналы, влияющие на значение сигнала min_half
. Данный сигнал подключен к выходу quotient
модуля half_divider
, поэтому мы будем смотреть исходный код данного модуля:
module half_divider(
input [31:0] numerator,
output[31:0] quotient
);
assign quotient = numerator << 1'b1;
endmodule
Что делает данный модуль? Он принимает на вход значение и делит его на два. На вход данного модуля будет приходить значение минимума из нашей формулы.
Выход данного модуля зависит от входа numerator
и логики сдвига влево на 1. Это значит, что проблема либо в логике, либо в значении, подаваемом на вход. Выведем сигнал numerator
на временную диаграмму и посмотрим на его значение в момент времени 10ns
:
Мы помним, что в момент, когда дизайн начал выдавать неправильный результат, на его входы подавались числа 1
и 1
, это значит, что на вход numerator
пришло корректное значение: минимум из этих двух чисел и правда равен 1
. Проверим логику данного модуля.
Исправление логики проблемного сигнала
Операция деления в цифровой схемотехнике является очень "дорогой" в плане ресурсов логических блоков и критического пути, поэтому этой операции часто стараются избегать. В нашем случае, нам не нужно обычное деление — нам нужно деление только напополам. В двоичной арифметике, для того чтобы разделить число на два, достаточно отбросить его младшую цифру. Вы часто пользуетесь подобной операцией в повседневной жизни при выполнении операции деления на 10: отбрасываете младшую цифру в десятичной арифметике.
Именно поэтому, когда мы в первый раз пытались посчитать результат "на бумаге", у нас было расхождение с моделью: когда мы делим 1 на 2, мы получаем 0.5, однако деление путем отбрасывания цифры округляет результат вниз (1/2=0, 15/10=1).
Как "отбросить" цифру средствами цифровой логики? Для этого используется операция сдвига вправо.
Операция сдвига вправо в SystemVerilog записывается оператором >>
. Справа от оператора указывается число "отбрасываемых цифр", в нашем случае одна. Но постойте, в логике присваивания стоит оператор <<
. Это ошибка, исправим ее!
Повторяем моделирование.
Снова на одну ошибку меньше. Не унываем, вряд ли в проекте число ошибок больше, чем число непустых строк самого проекта. Возвращаемся к начальной ошибке:
Мы продвинулись в во времени безошибочного моделирования до 15ns
, начинаем наше расследование с начала:
На вход дизайна подаются значения 3
и 4
, дизайн считает, что результатом вычисления max + min/2
будет 2
, модель считает, что 5
. Посчитаем сами:
max=4
min=3
max + min/2 = 4 + 3/2 = 4 + 1 = 5
И снова модель выдала правильный результат. Разберемся в значениях сигналов, формирующих сигнал abs
.
Проблема необъявленных сигналов
Поскольку на временной диаграмме стало уже очень много сигналов, уберем лишние, оставив только внутренние сигналы модуля vector_abs
:
В глаза сразу же бросается, что сигнал max
внешне отличается от всех остальных — он ведет себя как однобитный сигнал. Если все остальные сигналы 32-разрядные, то и сигнал max
должен быть таким же. Перейдем к объявлению этого сигнала, чтобы это исправить (нажав правой кнопкой мыши, и выбрав Go To Source Code
):
module vector_abs(
input [31:0] x,
input [31:0] y,
output[31:0] abs
);
wire [31:0] min;
wire [31:0] min_half;
max_min max_min_unit(
.a(x),
.b(y),
.max(max),
.min(min)
);
//...
Это странно, курсор был установлен на строку .max(max)
, хотя раньше в этом случае курсор устанавливался на строку, где объявлялся выбранный сигнал. Но вот в чем дело, если мы просмотрим файл внимательно, то не обнаружим объявления сигнала вовсе. Как так вышло, что мы использовали необъявленный сигнал, а САПР не выдал нам ошибку? Дело в том, что стандарт IEEE 1364-2005 для языка SystemVerilog допускает подобное использование необъявленного сигнала. В этом случае, синтезатор неявно создаст одноименный одноразрядный сигнал, что и произошло.
Для исправления этой ошибки, объявим сигнал max
с корректной разрядностью и повторим моделирование.
Самостоятельная работа
Число ошибок сократилось до 40! Мы явно на верном пути. Повторяем предыдущие шаги, вернувшись к первой ошибке:
В этот раз первая ошибка осталась прежней, только теперь дизайн считает, что результат должен равняться шести (в прошлый раз дизайн выдавал 2
). Мы уже убедились, что в этом случае модель дает правильный результат, поэтому сразу перейдем к формирующим результат сигналам:
Видим, что значение сигнала min_half
, формирующего значение выхода abs
неверно (минимумом из 3
и 4
является 3
, 3/2 = 1
).
Не отходя далеко от кассы, мы замечаем, что значение min
, формирующее сигнал min_half
неверно: его значение 4
, а должно быть 3
.
Используя файлы исходного кода проекта, попробуйте разобраться в последней обнаруженной нами ошибке.