Files
APS/Vivado Basics/Debug manual.md
2023-11-15 14:47:47 +03:00

38 KiB
Raw Blame History

Руководство по поиску и исправлению ошибок в проекте

Цель

При выполнении лабораторных работ вы непременно будете сталкиваться с множеством ошибок. И это нормально: "Не ошибается тот, кто ничего не делает" — © Джейсон Стейтем.
Важно воспитать в себе положительное восприятие обнаружения ошибок (ведь это приводит к улучшению вашего творения). Если относиться к обнаружению ошибок отрицательно, то вы подсознательно будете пытаться найти ошибки спустя рукава, но если вы "в домике", и ошибок не видите — это не значит что их нет.

При должном отношении, поиск ошибок может превратиться в увлекательное детективное расследование, где у вас есть "место преступления" (обнаруженное несоответствие в поведении, обычно это не сама ошибка, а ее следствие, круги на воде) и какой-то "набор улик" (фрагменты лога, исходный код). И вы по чуть-чуть будете разматывать "нераспутываемую паутину лжи", получая все новые улики, ведущие к истинной ошибке.

Этот документ посвящен практикуму по поискам подобных ошибок в SystemVerilog-коде.

Алгоритм поиска ошибок

  1. Обычно всё начинается с сообщения в логе тестов (никто не проверяет глазами временную диаграмму сложных проектов, состоящую из тысяч сигналов, меняющихся миллионы раз за микросекунду), но на наших простых лабах, этот шаг иногда может быть и пропущен.
    Сообщение в логе обычно содержит следующую ключевую информацию: имя сигнала, на котором установилось неверное значение, и время когда это произошло. Чем лучше написаны тесты, тем больше ключевой информации будет отражено в сообщении, поэтому написание тестов является своего рода искусством.
  2. Получив имя сигнала и время, мы отправляемся на временную диаграмму и проверяем нашу ошибку. Как это сделать? Необходимо определить по коду, какие сигналы и каким образом управляют нашим сигналом. Вариантов может быть несколько:
    1. Управляющие сигналы имеют корректное значение, но логика, по которой они управляют сигналом неверна, из-за этого на нем возникает неверное значение.
      Это идеальный случай, при возникновении которого мы сразу же находим причину проблемы и исправляем ее.
    2. Логика управления верна, а какая-то часть управляющих сигналов имеет неверное значение (пусть для примера, неверное значение будет на управляющем сигнале X). Это означает, что обнаруженное несоответствие сигналов является уже следствием какой-то ошибки, и мы должны вернуться к шагу 2, проверяя источники сигналов для сигнала X. Так происходит до тех пор, пока мы не попадаем в тип 1.
    3. Логика управления и значения управляющих сигналов верны. Это самый сложный тип ошибок, который заключается либо в ошибке в спецификации разрабатываемого устройства, либо в САПРе или компонентах, влияющих на его работу. В рамках данного курса вас не должны заботить данные ошибки, и при их возникновении вам стоит обратиться к преподавателю (предварительно убедившись, что ошибка совершенно точно не подходит под первые два варианта).
    4. Любая возможная комбинация всех предыдущих типов.
  3. Обнаружив первопричину ошибки, мы исправляем ее (возможно дополняя набор тестов, или внеся правки в спецификацию), и повторно запускаем все тесты, чтобы убедиться в двух вещах:
    1. ошибка действительно исправлена
    2. исправление ошибки не породило новых ошибок

Давайте отработаем эти шаги на примере отладки ошибок в проекте по вычислению приблизительной длины вектора.

Работа с логом при появлении ошибок

После запуска симуляции мы видим в логе множество ошибок:

waveform1

В любой ситуации с множеством ошибок, сначала надо разбираться с самой первой из них, поскольку она может быть ключом к появлению всех остальных. Поэтому листаем лог до момента первой ошибки:

waveform2

В логе сказано, что в момент времени 5ns, на дизайн подавались координаты вектора, равные 0 и 0, модель посчитала, что длина вектора равна нулю, в то время, как дизайн вернул значение x.

Поиск ошибки на временной диаграмме

Давайте найдем это место на временной диаграмме. Обычно, сразу после запуска симуляции на временной диаграмме отображено место, где симуляция остановилась (возможно с очень неподходящим масштабом). Для начала подгоним масштаб таким образом, чтобы вся временная диаграмма умещалась в окне. Это делается либо нажатием правой кнопкой мыши по в области отображения сигналов, с выбором "Full View" во всплывающем меню, либо нажатием на кнопку Затем найдем приблизительное место рядом с тем временем, что нас интересует, установим там курсор, и приблизим масштаб, периодически уточняя местоположения курсора, пока не найдем интересующее нас место.

waveform3

waveform4

waveform4

waveform5

waveform6

Мы видим ровно ту информацию, которую нам предоставил тестбенч. Теперь надо разобраться в причинах возникновения X-состояния. Такое может произойти в двух ситуациях: какой-то из сигналов, формирующих этот находится в X или Z состоянии, либо же два каких-то сигнала одновременно пытаются выставить разные значения (подобный вариант встречается куда реже и в цикле ваших лабораторных вряд ли встретится).

Открытие файла исходного кода проблемного сигнала

В любом случае, первым делом необходимо определить, источник формирования значения сигнала res. Для этого, откроем файл с исходным кодом, где определен данный сигнал. Для этого, нажмем правой кнопкой мыши по имени сигнала на временной диаграмме, и выберем Go To Source Code:

waveform7

Открывается следующий код (с курсором на строчке 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:

waveform8

Вообще говоря, мы уже видим, что выход abs (к которому подключен наш провод res) находится в X-состоянии, но для отработки навыков, разберемся с добавлением на временную диаграмму. Можно поступить двумя способами:

  1. Добавить все сигналы (то что видно в окне Objects на временную диаграмму) из окна Scope для этого, либо перетаскиваем нужный нам объект, зажав левую кнопку мыши на временную диаграмму, либо жмем правой кнопкой мыши по нужному объекту, и выбираем Add to Wave Window
  2. Добавить отдельные сигналы из окна Objects. Для этого выделяем их (возможно множественное выделение через модификаторы shift или ctrl), и как и в прошлом случае, либо перетаскиваем сигналы левой кнопкой мыши, либо добавляем их через правую кнопку мыши.

waveform9

waveform10

Обратите внимание, что часть сигналов отображают какое-то значение (сигнал abs отображает X-состояние), а часть не отображают ничего. Так произошло, потому что провод abs непрерывно связан с проводом res, с точки зрения симулятора это одна сущность, и записывая во время моделирования значения для сигнала res, симулятор неявно записывал значения для сигнала abs, чего не скажешь про остальные сигналы, которых не было во время моделирования на временной диаграмме.

Сброс симуляции и ее повтор, установка времени моделирования

Для того, чтобы получить отсутствующие значения, необходимо повторить моделирование. Для этого, необходимо сбросить время моделирования в 0 и запустить его снова.

Для этого, необходимо на панели симуляции нажать кнопку Restart (|◀), а затем кнопку Run all () или Run for (▶t)

Run for выполняет моделирование указанного количества времени, после чего моделирование приостанавливается. Моделирование может быть остановлено так же и вручную, либо вызовом соответствующей инструкции из кода теста.
Run all отличается от Run for тем, что в качестве количества моделируемого времени указывается "бесконечность", и моделирование будет остановлено только вручную, либо вызовом соответствующей инструкции.

Обратите внимание, что для добавления недостающих значений добавленных сигналов лучше всего выполнять описанную выше инструкцию. Аналогичного результата можно добиться и нажатием на кнопку Relaunch Simulation, однако эта команда запускает повторную компиляцию и запуск симуляции, что для крупных проектов выльется в потерю времени на излишнюю компиляцию.

waveform11

Панель управления симуляции с кнопками:

  1. Restart
  2. Run all
  3. Run for
  4. Relaunch Simulation

Кроме того, чтобы курсор и лог снова не ушли далеко от места первой ошибки, можно сразу указать, необходимое нам время моделирования перед выполнением команды Run for: 5ns

waveform12

В итоге видим следующую картину на временной диаграмме:

waveform13

Видим два сигнала в 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:

waveform14

Добавляем внутренние сигналы на временную диаграмму, и повторяем моделирование:

waveform15

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

waveform16

В логе сообщается о 102 найденных ошибках. Это ровно на одну ошибку меньше, чем было ранее. Это не означает, что в проекте осталось 102 ошибки, только то, что исправив данную ошибку, мы действительно что-то исправили, и теперь один из тестовых сценариев, который ранее завершался ошибкой, теперь завершился без нее. Помните, что если в проекте много ошибок, то часть ошибок может выправлять поведение других ошибок (хоть и не всегда, но иногда минус на минус может выдать плюс контексте ошибок проекта), поэтому надо осторожно полагаться на число найденных ошибок, если их больше нуля.

Посмотрим на нашу временную диаграмму снова, и выберем дальнейшие действия:

waveform17

Мы видим, что на временной диаграмме не осталось сигналов в X или Z-состоянии, а значит мы собрали все "низковисящие" улики нашего с вами расследования. Вернемся к месту преступления и попробуем поискать новые улики:

waveform18

Поиск ошибки в сигналах, формирующих проблемный сигнал

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

waveform19

Мы видим, что в момент времени 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:

waveform20

Мы помним, что в момент, когда дизайн начал выдавать неправильный результат, на его входы подавались числа 1 и 1, это значит, что на вход numerator пришло корректное значение: минимум из этих двух чисел и правда равен 1. Проверим логику данного модуля.

Исправление логики проблемного сигнала

Операция деления в цифровой схемотехнике является очень "дорогой" в плане ресурсов логических блоков и критического пути, поэтому этой операции часто стараются избегать. В нашем случае, нам не нужно обычное деление — нам нужно деление только напополам. В двоичной арифметике, для того чтобы разделить число на два, достаточно отбросить его младшую цифру. Вы часто пользуетесь подобной операцией в повседневной жизни при выполнении операции деления на 10: отбрасываете младшую цифру в десятичной арифметике.

Именно поэтому, когда мы в первый раз пытались посчитать результат "на бумаге", у нас было расхождение с моделью: когда мы делим 1 на 2, мы получаем 0.5, однако деление путем отбрасывания цифры округляет результат вниз (1/2=0, 15/10=1).

Как "отбросить" цифру средствами цифровой логики? Для этого используется операция сдвига вправо.
Операция сдвига вправо в SystemVerilog записывается оператором >>. Справа от оператора указывается число "отбрасываемых цифр", в нашем случае одна. Но постойте, в логике присваивания стоит оператор <<. Это ошибка, исправим ее!

Повторяем моделирование.

waveform21

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

waveform22

Мы продвинулись в во времени безошибочного моделирования до 15ns, начинаем наше расследование с начала:

На вход дизайна подаются значения 3 и 4, дизайн считает, что результатом вычисления max + min/2 будет 2, модель считает, что 5. Посчитаем сами:

max=4
min=3
max + min/2 = 4 + 3/2 = 4 + 1 = 5

И снова модель выдала правильный результат. Разберемся в значениях сигналов, формирующих сигнал abs.

Проблема необъявленных сигналов

Поскольку на временной диаграмме стало уже очень много сигналов, уберем лишние, оставив только внутренние сигналы модуля vector_abs:

waveform23

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

waveform24

Самостоятельная работа

Число ошибок сократилось до 40! Мы явно на верном пути. Повторяем предыдущие шаги, вернувшись к первой ошибке:

waveform25

В этот раз первая ошибка осталась прежней, только теперь дизайн считает, что результат должен равняться шести (в прошлый раз дизайн выдавал 2). Мы уже убедились, что в этом случае модель дает правильный результат, поэтому сразу перейдем к формирующим результат сигналам:

waveform26

Видим, что значение сигнала min_half, формирующего значение выхода abs неверно (минимумом из 3 и 4 является 3, 3/2 = 1).
Не отходя далеко от кассы, мы замечаем, что значение min, формирующее сигнал min_half неверно: его значение 4, а должно быть 3.

Используя файлы исходного кода проекта, попробуйте разобраться в последней обнаруженной нами ошибке.