Небольшое дополнение к посту про синхронность.
Правильный синхронный дизайн, задуманный сразу таким, как правило не вызовет каких-либо подозрений у синтезатора и он не будет ругаться. И даже если фиттер раскидает его по всему кристаллу, он все равно будет работать при разных условиях, если рабочая частота будет ниже той, что рассчитает TimeQuest в отчете FMax Summary. Однако, бывают ситуации, когда применение латчей в синхронном дизайне неизбежны, особенно, когда проект переносится с железа, вроде нашего случая. Здесь нам поможет гейтирование тактового сигнала - особой схемы внутри логического элемента ПЛИС:
Этот узел можно увидеть на картинках логических элементов выше. Он сделан специально и имеет защиту от метастабильности. В Верилоге этот вывод задействуется, если в списке чувствительности блока always будет более одного элемента. Однако, лично я не люблю когда always блок содержит что-то еще кроме тактовой частоты. Старайтесь обходиться только одним параметром. Так вот, мы можем использовать этот вывод как вход LE защелки, единственное условие это наличие более высокой тактовой частоты. В этом нам повезло: ядро PPU работает на частоте пиксельклока, что ровно в 4 раза меньше от входной частота для NTSC PPU и 5 раз выше для PAL версии. Таким образом, вот так мы эмулируем прозрачную защелку в синхронном дизайне:
Так же, в реальном мире используются часто RS триггеры из прошлого поста. Почему? Как правило, это сделано для синхронизации внешнего события к внутреннему распорядку схемы, которая живет строго по тактовой последовательности. В случае с PPU это интерфейс CPU. Действительно, даже при общей главной тактовой частоте, делители для ядра PPU и ядра CPU изначально разные. Каждое ядро работает на своей частоте и строго не синхронно на своем уровне. При этом, более медленное может пропустить событие от более быстрого. И вот здесь выручает RS триггер, который срабатывает практически мгновенно от внешнего сигнала и сохраняет свое состояние, пока внутреннее ядро не опознает сигнал и не среагирует на него. Оно же и снимает запрос с триггера. Выглядит это примерно вот так:
Здесь: зеленый это фронт тактовой частоты ядра, красный - ее спад. А набор W сигналов это управление извне. Нас интересует комбинаторное кольцо из элементов DATACTR_NOR3/4. Видно, вход S (вверху) синхронизирован к спаду тактовой частоты ядра, вход R подключен ко входному сигналу. Как это работает: лог.1 на W6_2 сбросит триггер и на верхнем элементе будет 1 а на нижнем 0. Этот 0 подается на NOR но так как W6_2 все еще действует, NOR будет продолжать выдавать лог.0 на защелку, которая будет повторять этот сигнал до синхронного тригера. Лог.0 на выходе уже синхронного триггера не может повлиять на исходный RS триггер потому, что на входе элемент AND. Так будет продолжаться до тех пор, пока W6_2 не уйдет в лог.0, сигнализируя об окончании запроса. Теперь 2 лог.0 (один со входа и другой с триггера) вызовут появление лог.1 на входе защелки, которая пробросит эту единичку на синхронный триггер. Этот триггер щелкнет такт и запишет эту единичку, которая вернется на вход S и при спаде тактовой частоты сформирует импульс длинной в пол периода таковой частоты, который вернет RS триггер в исходное состояние. Триггер обнулит нижний NOR и следующим тактом синхронный триггер запишет 0, а предыдущая лог.1 продолжительностью ровно 1 такт при этом уйдет в схему дальше. Вот так незатейливо сделана синхронизация по спадающему фронту не синхронного управляющего сигнала. По спаду обычно синхронизируют запись, т.к. данные следует сначала получить во входной буфер, а потом уже обработать. При чтении синхронизация может быть сделана по фронту, но сути это не меняет. Однако, Quartus'у это не нравится:
И это справедливо: мы нарушаем закон синхронности дизайна. А при этом корректная работа схема не гарантируется. Она может работать, но будет зависеть от фазы луны: чуть что-то добавил или исправил и все тайминги поехали. Когда я делал схемную интерпретацию схемы PPU влоб, чтобы получить хоть что-то наглядное и работоспособное, иногда приходилось вставлять задержку в одну ячейку (примитив LCELL), заставляя удлинять путь сигнала, чтобы тот приходит тогда, когда надо. Это не правильный путь, но это пока и не требовалось. Далее, схема потихоньку кромсалась, переводя тайминги в синхронный режим. И комбинаторное кольцо выше с помощью трюка повышенной частоты (напомню, у нас есть 4х или 5ти кратная частота от ядра) этот узел стал вот таким:
Здесь, зеленый все тот же позитивный пиксельклок, синий это повышенная частота. Функционал схемы работает так: пока W6_2 равен лог.0, сигнал на входе у нашей импровизированной защелке будет зависеть от выхода элемента OR, т.к. на нижнем выводе элемента AND будет лог.1 Элемент OR пропустит единичку либо с выхода защелки либо с выхода синхронного триггера, но в данный момент на синхронном триггере будет 0 а в защелке 1. Это устойчивое состояние, которое обновляется каждый такт повышенной частоты. Как только сигнал W6_2 станет лог.1, элемент AND обнулит свой выход из-за инверсии сигнала W6_2. Защелка запишет лог.0 на следующем такте (получается синхронный сброс) и дальше схема будет ждать деактивации сигнала W6_2, прямо как в предыдущем варианте схемы. Как только W6_2 станет равен лог.0, лог.1 появится на входе синхронного триггера, но на выход она попадет только строго по такту пиксельклока. Таким образом, элемент AND опять станет пропускать сигнал с OR, но там оба 0, так что в защелке сохранится лог.0. Как только синхронный триггер запишет лог.1, она появится на элементе OR и попадет на вход защелки. И она запишет ее на следующем своем такте но из-за того, что ее частота гарантированно в 2 раза выше тактовой частоты синхронного триггера, то синхронный триггер ровно на своем следующем такте подхватит лог.0, который будет результатом NOR операции над лог.1 с выхода защелки. Сама защелка станет себя удерживать, подавая себе на ход свой выход через OR и AND, вызывая устойчивое состояние. Вот так, легким движением руки мы сделали синхронную защелку. На самом деле это очень красивое решение, которое на Верилоге можно записать так:
Код:
wire W6_2; // Входной сигнал
reg Str; // Синхронный триггер
reg Sgt; // Синхронная защелка
always @(posedge HClk)
begin
Sgt <= ~W6_2 & (Str | Sgt);
end
always @(posedge PClk)
begin
Str <= ~(Sgt | W6_2);
end
Это красивый и правильный путь, понятный нативно самой ПЛИС и инженеру-цифровику. Однако, многие из вас простые люди, либо программисты. Поэтому, вы, скорее всего, запишете вот так:
Код:
wire W6_2; // Входной сигнал
reg Str; // Синхронный триггер
reg Sgt; // Синхронная защелка
always @(posedge HClk)
begin
if (Str) Sgt <= 1'b1;
else if (W6_2) Sgt <= 1'b0;
end
always @(posedge PClk)
begin
Str <= ~(Sgt | W6_2);
end
Так, конечно, стало более понятно простому человеку, однако синтезатор начнет городить огород: закольцованные мультиплексоры. Вроде вот таких:
Схема остается строго синхронной. Макроблоки, создаваемые синтезатором (мультиплексоры, сумматоры и прочее), как правило, имеют оптимизацию на стадии компиляции под выбранное семейство ПЛИС, однако, если у вас примитивное CPLD, это может отобрать целую ячейку, когда простая ручная комбинаторика OR-AND укладывается в стандартную логическую матрицу даже примитивного по нынешним меркам семейства MAX3000. Так что мозги и правильное решение иногда позволит работать вашему проекту там, где обычные индусы будут требовать жирную FPGA.
Вывод из этого поста следующий: если вы собираетесь профессионально писать для ПЛИС, постарайтесь вникнуть в философию логики, изучайте состав сложных макроблоков. Они всегда описывались в советских справочниках и даже в зарубежной библии от TI - у меня есть такая, просто мегакнижка по х4ххх серии. Например, тот же мультиплексор это всего-лишь 2AND-OR цепочка, которая сможет быть свернута оптимизатором в один LUT гарантированно.