Skip to content

Latest commit

 

History

History
508 lines (402 loc) · 28.5 KB

gtkwave-filters.org

File metadata and controls

508 lines (402 loc) · 28.5 KB

Об использовании фильтров в GTKWave

Введение

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

В этой статье мы кратко рассмотрим работу с фильтрами тансляции и транзакционными фильтрами в GTKWave, а также разберем практические примеры их использования.

После прочтения этой статьи вы сможете самостоятельно написать фильтр и применить его в своей работе.

Translate Filter

Translate Filter - это простой фильтр, заменяющий одно значение на другое. На вход фильтра подаётся значение, взятое из диаграммы, а на выходе программа ожидает строку, которая будет использована для замены этого значения. Значение на вход подаётся в виде строки с теми же символами, который вы видите на диаграмме. Т.е. если на диаграмме сигнал отображается в виде шестнадцатеричной строки, но на вход фильтра придёт именно эта строка с шестнадцатеричными символами.

Translate Filter File

Файловый транслятор - это самый простой фильтр, который представляет собой файл с двумя колонками, разделенными пробелом. В первой колонке строка, которую нужно заменить (ключ), вторая колонка - строка замены.

Например, у нас есть конечный автомат с такими состояниями:

enum int unsigned {
    ST_IDLE = 0,
    ST_CHECK_ADDR,
    ST_RECEIVE_DATA,
    ST_SEND_ACK,
    ST_DONE
} state;

Если установить формат данных Decimal, то на диаграмме это будет выглядеть так, как показано на рис.1.

./images/translate-file-wave0.png

Создадим файл фильтра state-filter.txt:

0 IDLE
1 CHECK_ADDR
2 RECEIVE_DATA
3 SEND_ACK
4 DONE

А теперь применим его для сигнала state и посмотрим результат. Нужно отметить не очень интуитивное поведение диалога добавления фильтра, которое в первый раз может вызвать непонимание. Для применения фильтра нужно правой кнопкой щёлкнуть на сигнал и в меню выбрать “Data Format/Translate Filter File/Enable and Select” (то же самое можно сделать через меню “Edit”). В открывшемся диалоговом окне добавить в список нужный файл кнопкой “Add Filter to List” (если он там уже есть, добавлять не нужно), а затем выбрать его в списке и нажать “OK”. Если файл в списке не будет выбран, фильтр не будет применен к сигналу.

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

./images/translate-file-wave1.png

Если изменить формат представления данных на HEX, то значения будут отображаться в виде шестнадцатеричных строк (например, 00000001), потому что таких ключевых строк в фильтре нет. Если вернуть обратно десятичный формат, то значения снова будут отображаться в виде названий состояний автомата.

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

Поиск осуществляется с первой по последнюю строку до совпадения ключа. Соответственно, если в файле несколько строк с одинаковым ключом, будет выбрана первая.

Кроме подстановки текста, программа позволяет раскрасить фон диаграммы. Для этого перед строкой замены нужно указать название цвета, обрамленное в знаки вопроса. Например, если заменить строки IDLE и RECEIE_DATA на показанные ниже, то соответствующие участки диаграммы раскрасятся в указанные цвета (рис.3).

0 ?blue violet?IDLE
2 ?brown4?RECEIVE_DATA

./images/translate-file-wave2.png

В палитре больше 700 цветов. Посмотреть названия их всех можно в исходниках в файле src/rgb.c (в стабильной версии) или в файле lib/libgtkwave/src/gw-color.c (https://github.com/gtkwave/gtkwave/blob/master/lib/libgtkwave/src/gw-color.c) в последней версии на день написания статьи.

Translate Filter Process

Если простого текстового фильтра не хватает, например, если нужно выполнять какие-то вычисления, то можно использовать фильтр на основе внешней программы или скрипта. Такой фильтр называется “Translate Filter Process”.

Принцип простой: GTKWave запускает внешнюю программу и при каждой перерисовке диаграммы запрашивает у неё замены для отображаемых значений. Запросы поступают на стандартный вход в виде строк, оканчивающихся символом перевода строки, а со стандартного вывода ожидает ответ в виде строки для замены. На каждый запрос должен быть отправлен ответ. Важно отметить, что после ответа программа должна принудительно сбросить буферы вывода (flush), иначе GTKWave зависнет в ожиданиии ответа.

Запросы в программу поступают при каждой перерисовке диаграммы. По завершении работы GTKWave закрывает исходящий поток, а программа получает код “End Of File”.

Формат выходной строки такой же, как в текстовом фильтре. Это касается и способа раскраски фона.

Для примера попробуем раскрасить диаграмму исходя из значения знакового вектора. Если значение меньше нуля - синий, если от нуля до трёх, то оставим как есть, если 4 и больше - коричневый. А если это не десятичное число, то выведем строку “NaN” на красном фоне.

Вот скрипт на питоне, который выполняет такую фильтрацию:

#!/usr/bin/env python

import sys

for line in sys.stdin:
    try:
        key_value = int(line)

        if key_value < 0:
            print("?blue4?{}".format(key_value))
        elif key_value < 4:
            print(key_value)
        else:
            print("?brown4?{}".format(key_value))

    except ValueError:
        print("?dark red?NaN")

    sys.stdout.flush()

Снова напомню, что после каждой строки необходимо сбрасывать буфер - sys.stdout.flush().

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

./images/translate-proc-wave0.png

И опять, как и в предыдущем случае, чтобы всё работало корректно, необходимо установить формат сигнала в “Signed Decimal”, т.к. на вход фильтра подаются строки в таком виде, в котором они отображаются на диаграмме.

Transaction Filter

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

Для разбора транзакций в GTKWave добавили ещё один тип фильтров, который называется Transaction Filter Process. Работает он почти так же, как Translate Filter Process, но принимает на вход упрощенный дамп VCD с нужными сигналами, и возвращает некое подобие VCD с новыми сигналами, которые будут добавлены на диаграмму. Т.е. фильтр имеет доступ ко всей и истории, позволяя разобрать как отдельные значения сигналов, так и транзакции.

UART

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

На рис.5 показана диаграмма с двумя сигнала передачи UART на разных скоростях. Формат передачи: 8 бит, 1 стоп-бит, без контроля чётности. Попробуем определить скорость передачи и узнать, что было передано.

./images/uart-raw.png

Итак, как было сказано выше, GTKWave передаёт на вход фильтра дамп VCD с сигналами, для которых был применён фильтр. Формат дампа соответствует стандарту VCD, но для упрощения тэги в нём не переносятся на новую строку, т.е. при разборе можно не искать закрывающий тэг $end, а полагаться только на открывающий. Кроме того, строки не могут содержать пробелов в начале и в конце, а между словами только один пробел, что тоже упрощает разбор.

GTKWave присылает дамп в виде блоков, начинающихся с комментария data_start и заканчивающихся комментарием data_end. Шестнадцатеричное число внутри комментария - эти уникальный идентификатор блока, который не используется при фильтрации и предназначен только для отладки (на самом деле это значение указателя на структуру внутри программы).

$comment data_start 0x39f9a40 $end
... тело запроса ...
$comment data_end 0x39f9a40 $end

В блоке находятся данные о состояниях сигналов, для которых был применен фильтр. При этом, если просто применить фильтр для нескольких сигналов, то эти сигналы будут переданы по отдельности, каждый в своём блоке. Если нужно разбирать шину, состоящую из нескольких сигналов, то эти сигналы нужно сначала объединить с помощью команды “Edit/Combine Down”, а затем уже применять к ним фильтр.

После получения каждого такого блока необходимо сформировать ответ, содержащий список новых сигналов с их значениями. Список состоит из блоков, начинающихся с тэга $name и заканчивающихся тегом $next, если блок не последний, или $finish, если в списке больше нет сигналов.

$name signal0_name
...
$next
$name signal1_name
...
$finish

Сделаем каркас для фильтра и выведем в поток стандартного вывода ошибок (stderr) дамп того, что нам прислал GTKWave. Так мы сможем своими глазами увидеть формат запроса, поступающего от GTKWave. Чтобы GTKWave не зависла, сформирует и выведем в поток стандартного вывода минимальный ответ. Без этого программа зависнет и её придется принудительно перезагружать.

Максимально упростим разбор и будем определять границу блока только по комментарию data_end.

#!/usr/bin/env python

import sys

# Версия print c принудительным сбросом буфера stdout
def pprint(*args, **kwargs):
    print(*args, **kwargs)
    sys.stdout.flush()

for line in sys.stdin:
    # Выводим дамп в stderr, чтобы посмотреть, что нам прислал GTKWave
    sys.stderr.write(line)

    # По окончанию приёма блока пошлём ответ
    if (line.startswith("$comment data_end")):
        pprint("$name New Signal")
        pprint("#0 ?dark cyan?Just Text")
        pprint("$finish")

Здесь стоит напомнить, что после вывода каждой строки необходимо сбрасывать выходной буфер. Для этого в коде используется функция pprint, которая делает то же самое, что print, но после вывода сбрасывает выходной буфер функцией sys.stdout.flush().

Применим фильтр к сигналу, и посмотрим, что получилось на рис.6.

./images/uart-step0.png

Как видим, сигнал tx0 был просто заменён на новый. Если из фильтра напечатать ещё один сигнал, то он отобразится ниже. Но предварительно необходимо добавить на диаграмму несколько пустых строк (“Insert Blank”), иначе новый сигнал некуда будет вставить. Если вы хотите, чтобы новый сигнал отображался рядом, а не заменял старый, можно вызвать команду “Combine Down”, а затем уже применить фильтр.

В поток стандартных ошибок мы вывели дамп запроса от GTKWave. Вот он (показан не полностью):

$comment name tx0 $end
$timescale 1ps $end
$comment min_time 0 $end
$comment max_time 64134 $end
$comment max_seqn 1 $end
$scope module uart $end
$comment seqn 1 uart.tx0 $end
$var wire 1 1 tx0 $end
$upscope $end
$enddefinitions $end
#0
$dumpvars
11
$end
#3456
01
#5456
11
...
#44456
01
#45456
11
#64134
$comment data_end 0x1d95c10 $end

Сделаем минимальный разбор входного VCD и сложим значения сигнала с временными метками в массив в виде кортежей (tuple). Так же, извлечём имя сигнала с индексом 1, чтобы потом использовать его в имени нового сигнала.

sig_name = ""
sig_smpl = []
timestamp = 0

for line in sys.stdin:
    # Извлекаем имя сигнала с индексом 1
    if line.startswith("$comment seqn 1"):
        sig_name = line.split()[3]
    # Временная метка
    elif line[0] == '#':
        timestamp = int(line[1:])
    # Добавляем сигнал в массив
    elif (line[0] == '0' or line[0] == '1') and line[1] == '1':
        sig_smpl.append((timestamp, int(line[0])))

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

После приёма блока выведем первую строку ответа с именем нового сигнала:

# По окончанию приёма блока пошлём ответ
if line.startswith("$comment data_end"):
    pprint("$name {} (flt)".format(sig_name))

Теперь нужно определить скорость передачи. Сделаем предположение, что в достаточно длинной посылке с высокой вероятностью найдется байт данных с комбинацией 101 или 010. Длительность этого одинокого нуля или единицы примем за длительность бита. Т.е. в нашем массиве найдем переход от 0 к 1 или наоборот с минимальной длительностью.

# Найдем минимальное время между переключениями сигнала
t = sig_smpl[0][0]
s = sig_smpl[0][1]
dt = None # здесь будет длительность бита

for smpl in sig_smpl:
    if s != smpl[1]:
        dt_new = smpl[0] - t
        dt = dt_new if dt is None else min(dt, dt_new)
        t = smpl[0]
        s = smpl[1]

Остаётся найти первый стартовый бит (переход от 1 к 0) и относительно его середины прочитать значения следующих 8 бит. Это будет байт переданных данных.

smpl_idx = 0
t_start = None
tm = 0

while True:
    # Ищем стартовый бит
    start = get_following(tm, 0)
    if start is None:
        break

    # Ставим указатель времени на середину стартового бита
    tm = start[0] + dt * 0.5
    b = 0

    # Читаем значение байта
    for n in range(8):
        tm = tm + dt
        b = (b >> 1) | (get_signal(tm) << 7)

    # Ищем стоповый бит
    stop = get_following(tm, 1)
    if stop is None:
        stop = sig_smpl[-1][0]

Примечание: код вспомогательных функций в конце статьи.

После получения байта можно сразу выдать ответ:

# Выводим сигнал
pprint("#{} {}".format(start[0], chr(b) if b >= 32 and b <=127 else str(b)))
pprint("#{}".format(stop[0] + dt))

Первая строка возвращает время начала стартового бита и значение полученного байта (символ ASCII или код, если символ непечатный). Вторая строка печатает только время окончания стопового бита без значения.

Затем возвращаемся к поиску следующего стартового бита и повторяем процедуру, пока не закончатся элементы массива.

Т.к. новый сигнал один, после окончания разбора выведем строку $finish и обнулим массив сэмплов для разбора следующего блока:

pprint("$finish")
sig_smpl = []

Результат применения фильтра показан на рис.7.

./images/uart-step1.png

Диаграммы сигналов были заменены на их отфильтрованные версии. Если сделать копию каждого сигнала через “Combine Down” и затем применить фильтры, то получим картину, показанную на рис.8.

./images/uart-step2.png

И в заключение код вспомогательных функций:

# Возвращает значение сигнала в момент времени
def get_signal(tm):
    s = 0
    for smpl in sig_smpl:
        if smpl[0] <= tm:
            s = smpl[1]
        elif smpl[0] > tm:
            break

    return s

# Возвращает элемент массива sig_smpl, время которого >= tm, а значение равно val
def get_following(tm, val):
    smpl = None
    for n,s in enumerate(sig_smpl):
        if s[0] >= tm and s[1] == val:
            smpl = s
            break

    return smpl

AXI

С помощью фильтра транзакций можно разбирать не только такие простые шины, как UART, но и сложные. Такие, например, как транзакции на шине AXI.

Для примера попробуем разобрать несколько транзакций в канале чтения на шине AXI4. Для наглядности, и чтобы не усложнять код фильтра, не будет рассматривать все сигналы, возьмём только часть.

./images/transaction-init.png

На рис.9 изображена диаграмма с 6 транзакциями на шине AXI. Первые две со значением arid 0 и 1 - корректные транзакции чтения, ответы на которые приходят в обратном порядке - сначала данные для транзакции 1, а затем для транзакции 0.

Далее идут 4 некорректных запроса:

  • Сигнал arvalid снимается до прихода arready;
  • Burst-транзакция пересекает границу в 4к;
  • Сигнал arready имеет неопределенное значение во время активности arvalid;
  • Иксы на сигнале araddr.

Код для полноценного разбора транзакий на шине получился достаточно объёмный, по этому в статье его приводить не будем. Просто посмотрим на результат, который показан на рис.10.

./images/transaction-filtered.png

Как можно увидеть, что фильтр вернул два виртуальных сигнала с транзакциями в канале адреса чтения и в канале чтения данных. Неупорядоченные транзакции были обнаружены и корректно раскрашены с соответствии с сигналами arid и rid. Некорректные транзакции были распознаны и обозначены в соответствии с типом ошибки.

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

Заключение

Фильтры в программе GTKWave представляют достаточно мощные средства для обработки и визуализации временных диаграмм. Простые фильтры трансляции позволяют без труда выполнить замену цифровых значений на диаграмме на понятный человекочитаемый текст. В то же время, для сложной фильтрации с сохранением контекста и с учётом зависимости между сигналами можно использовать транзакционные фильтры. Их сложность компенсируется универсальностью и гибкостью.

Исходные коды вы можете найти на странице проекта на GitHub: https://github.com/punzik/gtkwave-filters-article