В предыдущих частях цикла мы разбирали работу с платой Tang Nano 20K как в фирменных инструментах GOWIN, так и в наборе open-source-утилит. Открытые инструменты хороши тем, что позволяют симулировать и отлаживать проект ещё до его заливки в реальную микросхему. Однако, как мы увидим, в состав инструментов GOWIN входит очень простой в освоении и при этом мощный встроенный логический анализатор.
Настройка
Для начала понадобится конфигурационный GAO-файл. При его создании в редакторе доступно множество сложных параметров. Пока в проекте есть активный GAO-файл, инструмент встраивает логический анализатор прямо в вашу схему, а сам файл задаёт условия запуска (триггеры).
Полезный совет: активным может быть только один GAO-файл одновременно. Иногда логический анализатор не нужен, а иногда требуется альтернативная конфигурация. Если щёлкнуть правой кнопкой мыши почти по любому файлу во вкладке Design, появится возможность отключить его: файл помечается большим красным крестиком и исключается из последующих сборок — пока вы не включите его снова. Так удобно переключаться между несколькими GAO-файлами и даже несколькими Verilog-файлами для конкретной сборки (например, одним для тестов и другим для продакшена).
Если создавать GAO-файл с нуля, инструмент спросит, нужен ли «облегчённый» режим (Lite Mode). На практике он не нужен почти никогда — всегда выбирайте Standard Mode. Кроме того, почти всегда стоит выбирать RTL-режим: он показывает (по большей части) те сигналы, которые вы объявили сами, а не служебные, добавленные во время обработки.
Полное руководство, разумеется, доступно онлайн. Но видео ниже и дальнейшая часть материала помогут быстро начать.
Основы GAO
Чтобы настроить зондирование сигналов, нужно понимать саму схему. Поэтому дальше мы разберём Verilog-код, на котором будем проверять работу логического анализатора, а результат будет показан в видео. Вот пара предварительных примеров того, как в итоге выглядит вывод анализатора.
Левое изображение показывает импульс, переключающий светодиоды, — и это нетривиально, ведь тактовая частота дискретизации составляет 27 МГц. Правая часть показывает последовательные данные и использует более медленный такт, специально выделенный для отладки.
Внутри Verilog
Verilog бывает коварен. Если вы уже умеете программировать, он выглядит достаточно похоже на язык программирования, чтобы подтолкнуть думать о нём неправильно. Verilog нельзя воспринимать как код: он описывает желаемое поведение аппаратуры. Ключевую роль в этом играют тактовые сигналы.
У большинства практических цифровых систем есть тот или иной тактовый сигнал, который заставляет всё работать. Совсем простым схемам, как в прошлом примере, он может быть не нужен, но практическим системам — почти всегда. Теоретически можно строить и асинхронную логику, но это очень сложно. Синхронная логика — та, где всем управляет такт, — встречается несравнимо чаще.
Зачем нужны такты?
Мы привыкли думать, что сигналы меняются мгновенно, но это не так. Они меняются в слегка разные моменты по множеству причин: длина проводника, ёмкость, различия в конструкции элементов, температура и, вероятно, многое другое. Разберём утрированный пример. Представьте очень безопасный автомобиль: если он замечает, что двигатель работает и дверь открыта, он глушит мотор. Это логический элемент И (AND). Для наглядности предположим, что сигналы от двери и от двигателя доходят до него со случайной задержкой от 100 до 900 миллисекунд.
Обычно это не имело бы большого значения. Вы закрываете дверь, смотрите в зеркало, включаете плейлист, заводите машину. Но представьте, что вы спешите: запрыгиваете в салон и тут же пытаетесь завести двигатель. Сигнал «двигатель запущен» может прийти раньше, чем успеет погаснуть сигнал «дверь открыта». И мотор глохнет сразу после запуска. Не очень-то приятно.
Пример надуманный, но проблема реальна. Допустим, вы складываете два числа. Одно меняется с 11 на 22, другое — с 5 на 10. Вы ждёте результаты 16 и 32. А вместо этого получаете 16, затем 27 и только потом 32. Или, может быть, 16, 21 и 32. Тоже так себе.
Такты как раз и дают всему устояться, а затем вычислить выход. Эти выходы остаются стабильными до следующего такта. Это грубое упрощение, но подробнее поговорим позже. Каждый элемент схемы тратит на срабатывание какое-то время, и самое большое из этих времён обычно и задаёт максимальную тактовую частоту: если какая-то часть схемы надёжно меняет состояние за микросекунду, период такта придётся сделать длиннее этого времени.
Разберём пример файла. Это обычный Verilog, хотя у SystemVerilog (файлы .sv) есть преимущества, о которых будет сказано отдельно. Наш пример — очень простая схема, просто управляющая несколькими светодиодами на плате 20K. Входная тактовая частота — 27 МГц, поэтому, чтобы светодиоды мигали, придётся считать множество тактов и менять их состояние лишь изредка.
Кроме того, в проекте используется IP — готовые предопределённые блоки — для построения UART. Функция простая и условная: по умолчанию введённый через UART символ просто отражается обратно (эхо). Но если нажать пользовательскую кнопку на плате, устройство переключится в режим, где к символу перед отправкой прибавляется единица; повторное нажатие возвращает обычное эхо. В любом случае при нажатии кнопки устройство отправляет звёздочку.
Сначала — шапка файла
В начале файла вы увидите такую строку:
default nettype none
Это хорошая практика, потому что без неё Verilog с радостью создаст новые цепи (nets) из опечаток. Например:
countt=count+1;
Теперь count не меняется, зато появляется новый провод countt, который задаётся, но нигде не используется. Проблема лишь в том, что некоторые IP-блоки GOWIN ожидают тип цепи по умолчанию wire, поэтому в конце файлов его нужно вернуть:
default nettype wire
Ещё одна особенность: некоторые инструменты понимают, что если объявлен вход или выход, то по умолчанию подразумевается wire, если не указано иное. Инструменты GOWIN откажутся это делать, если тип по умолчанию задан как none.
Модульность
Следующая часть — модуль led. В теории это переиспользуемый блок, но одновременно он является и нашим верхним (top) модулем. Синтаксис #() позволяет задать параметры, которые можно менять при создании экземпляра модуля, а также их значения по умолчанию. Например, в прежнем тестбенче использовалась такая запись:
led #(.CLOCK_FREQUENCY(10, .INTERVAL_MS(1000)) uut(.reset(reset), .Clock(clk)...
Здесь для целей симуляции задаются медленный такт и интервал вместо значений по умолчанию. Параметры есть не у всех модулей. Дальше идёт то, что выглядит как список аргументов из входов и выходов. Это порты, которые можно использовать, если модуль не верхний; но поскольку этот модуль — верхнего уровня, нам понадобятся ограничения (constraints), чтобы назначить их конкретным выводам FPGA.
Операторы localparam похожи на #define в C — с их помощью мы задаём нужный счётчик и вычисляем число битов для него (функция $clog2 возвращает двоичный логарифм на этапе компиляции).
Большую часть времени мы имеем дело с проводами (wire). Их обязательно что-то должно «питать» — они идеальны для соединений. Но есть ещё и regs. Можно подумать, что это триггеры (flip-flops), но это не совсем так: это просто переменные Verilog, если угодно.
Легко представлять wire и reg как 0 или 1, но на самом деле Verilog поддерживает и значения вроде X (неизвестно) и Z (нечто вроде открытого коллектора). Более того, SystemVerilog позволяет писать logic вместо wire или reg — об этом ещё поговорим. Однако «чистого» Verilog в мире так много, что понимать его важно, даже если в своих проектах вы планируете использовать более современный SystemVerilog.
Посмотрите на эту строку:
Генератор IP-блоков предлагает множество настроек и ссылки на документацию.
reg [COUNT_BITS-1:0] interval_count;
Это говорит, что у нас есть reg по имени interval_count. Его биты идут от бита 0 (младшего) до COUNT_BITS-1. При необходимости порядок можно перевернуть или, например, начать с бита 1. Это не массив (хотя массивы Verilog тоже поддерживает), а многобитная переменная. Если биты не указывать (как в идущем следом определении interval_tick), переменная будет однобитной.
Следующий фрагмент кода определяет переменные reg и wire, которые подключатся к UART. Пока мы не строим собственный UART, а используем IP-блок от GOWIN. Загвоздка в том, что он рассчитан на подключение к микропроцессору, поэтому мы немного схитрим, чтобы упростить работу с ним. Вставить IP в проект можно из меню Tools → IP Core Generator.
Финальная секция настройки обрабатывает вход кнопки и состояние светодиодов. Кнопки не синхронизированы с тактом, и это может привести к серьёзной проблеме. Вдобавок механические переключатели «дребезжат»: они переходят не из «выкл» в «вкл» одним махом, а так — выкл, вкл, выкл, вкл, выкл, вкл, выкл — и лишь через несколько миллисекунд устаканиваются в положении «вкл». Для борьбы с этим есть модуль антидребезга (debounce). Пока мы будем использовать его так же, как и UART, хотя он лежит в файле debounce.v.
Создание экземпляра
Вот как создаётся модуль антидребезга. В теории их можно создать сколько угодно:
debounce #(
.CLOCK_FREQUENCY (CLOCK_FREQUENCY),
.DEBOUNCE_MS (10)
) button_filter (
.Clock (Clock),
.reset (reset),
.noisy_button (pushbtn),
.clean_button (button)
);
Имя экземпляра — button_filter, а параметрами мы сообщаем ему тактовую частоту и время антидребезга, равное 10 миллисекундам. Подключаются линии такта и сброса, а также вход кнопки и её «очищенная» версия на выходе.
Присваивание: UART
Есть несколько строк, начинающихся с assign. Это «паяльники» Verilog: вы подключаете выходной провод к какой-то переменной или выражению, и далее этот провод будет именно таким. Мы уже видели это в более ранних демо только со светодиодами.
Затем идёт куда более сложное создание экземпляра UART. Хотя у него больше входов и выходов, по сути это то же самое, что и подключение модуля антидребезга. Обратите внимание, что в некоторых случаях на вывод просто подаётся фиксированное значение (например, 3'b001 — трёхбитная двоичная константа, или 8'hff — восьмибитное шестнадцатеричное FF). Часть подключений — это wire или reg, часть — выражения, например ~reset (инверсия сброса).
Итог прост: когда uart_rx_ready_n переходит в низкий уровень, из uart_rx_data можно прочитать символ. Для передачи устроено похоже.
Триггер (flip-flop)
В остальной части файла есть три блока always. С always можно делать две вещи (в SystemVerilog их, как мы увидим в будущем, даже разделяют). Пока сосредоточимся на том, как писать тактируемые триггеры через always @.
Самый простой — последний, который переключает состояние светодиода:
always @(posedge Clock) begin
if (reset)
led_state <= 1'b0;
else if (interval_tick)
led_state <= ~led_state;
end
Так создаётся триггер, срабатывающий по положительному фронту Clock. Если линия reset в высоком уровне, триггер установит led_state в 0. Иначе он установит состояние светодиода в инверсию того, что было раньше.
Здесь стоит отметить пару моментов. Во-первых, led_state нельзя задавать нигде, кроме как внутри этого блока always. Вторая тонкость сложнее. Обратите внимание, что в присваиваниях используется <=, а не =. Можно написать и =, но это ведёт к проблеме. Представьте такой код внутри блока always:
qout=3'b000; if (b) quot=~quot;
Verilog истолкует это так, что qout должен быть установлен в 0 до следующей строки. Но помните: во время «выполнения» Verilog не идёт по коду строка за строкой. Поэтому он выведет защёлку (latch), которая удержит это значение и подаст его в логику, обрабатывающую вторую строку.
Это не только не то, что вам нужно, но и порождает на устройстве странные конструкции, работающие неэффективно. Вместо этого используйте присваивание <=. По сути, каждое значение quot, прочитанное в блоке always, будет тем, каким quot было на фронте такта. А в конце блока «побеждает» последнее присваивание через <=.
Скорее всего, именно это вы и имели в виду, и такой подход избавляет от создания странных защёлок, которые в лучшем случае замедлят вашу схему. На практике это можно представлять так: триггеры захватывают входы по фронту такта, а затем выдают выходы, готовые (как минимум) к следующему фронту.
Сигнал должен оставаться стабильным от времени предустановки (setup) до времени удержания (hold).
Метастабильность
Как уже отмечалось, модуль антидребезга делает две вещи: устраняет дребезг механического переключателя и синхронизирует его с тактом. Проблема в том, что мы представляем триггеры идеальными, но они, конечно, не таковы. У любого реального триггера есть время предустановки (setup) и время удержания (hold). Представьте очень плохой триггер со временем установки 100 мс и временем удержания 50 мс: это значило бы, что вход должен быть стабилен не менее 100 мс до тактового фронта и не может меняться ещё 50 мс после него.
Это одна из причин, ограничивающих максимальную тактовую частоту: логике нужно время, чтобы стабилизироваться до прихода такта. Но если сигнал приходит из другого тактового домена или, что ещё хуже, асинхронно, возникает риск. Если требования триггера не выполнены, он может уйти в метастабильное состояние — оказаться в неопределённом положении или даже дико осциллировать.
Самый распространённый способ борьбы — задержать сигнал несколькими триггерами. Идея в том, что даже если один триггер уйдёт в метастабильность, это не распространится по всей системе. Просто имейте это в виду, когда работаете с чем-то асинхронным или пытаетесь соединить триггеры из одного тактового домена с триггерами из другого.
Если картинка стоит тысячи слов…
Теперь, когда понятно, что происходит в Verilog, можно переходить к настройке логического анализатора. Как это работает, показано в видео ниже. Файлы проекта также доступны на GitHub.
А тем, кому нужно освежить основы цифровой логики, помогут вводные курсы и «буткемпы» по FPGA.













