23 сентября 2014 в 14:59

Пару слов о конвейерах в FPGA

Всем привет!

Многим известно, что во всех современных процессорах есть вычислительный конвейер. Бытует заблуждение, что конвейер — это какая-то фишка процессоров, а в чипах для других приложений (к примеру, сетевых) этого нет. На самом деле конвейеризация (или pipelining) — это ключ к созданию высокопроизводительных приложений на базе ASIC/FPGA.

Очень часто для достижения высокой производительности выбирают такие алгоритмы, которые легко конвейеризируются в чипе. Если интересно узнать о низкоуровневых подробностях, добро пожаловать под кат!


Простой пример


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

Вполне очевидный код на языке SystemVerilog для этой задачи:
module no_pipe_example(
  input              clk_i,

  input        [7:0] x_i [3:0],

  output logic [7:0] no_pipe_res_o
);
// no pipeline
always_ff @( posedge clk_i )
  begin
    no_pipe_res_o <= ( x_i[0] + x_i[1] ) +
                     ( x_i[2] + x_i[3] );
  end
endmodule


Взглянем на получившуюся схему из регистров и сумматоров. Используем для этого RTL Viewer в Quartus'e. (Tools -> Netlist Viewer -> RTL Viewer). В рамках этой статьи слова «регистр» и «триггер» являются полноценными синонимами.



Что получилось?
  1. Входы x_i[0] и x_i[1] подаются на сумматор Add0, а x_i[2] и x_i[3] на Add1.
  2. Выходы c Add0 и Add1 подаются на сумматор Add2.
  3. Выход с сумматора Add2 защелкивается в триггер no_pipe_res_o.

Добавим регистры между сумматорами Add0/Add1 и Add2.

module pipe_example(
  input              clk_i,

  input        [7:0] x_i [3:0],

  output logic [7:0] pipe_res_o
);
// pipeline
logic [7:0] s1 = '0;
logic [7:0] s2 = '0;

always_ff @( posedge clk_i )
  begin
    s1         <= x_i[0] + x_i[1];
    s2         <= x_i[2] + x_i[3];
    pipe_res_o <= s1 + s2;
  end
endmodule




  1. Входы x_i[0] и x_i[1] подаются на сумматор Add0, а x_i[2] и x_i[3] на Add1.
  2. После суммирования результат кладется в регистры s1 и s2.
  3. Данные с регистров s1 и s2 подаются на Add2, результат суммирования защелкивается в pipe_res_o.

Для того чтобы увидеть разницу в поведении модулей no_pipe_example и pipe_example объединим их в один и просимулируем.
Скрытый текст
module pipe_and_no_pipe_example(
  input              clk_i,

  input        [7:0] x_i [3:0],

  output logic [7:0] no_pipe_res_o,
  output logic [7:0] pipe_res_o
);
// no pipeline
always_ff @( posedge clk_i )
  begin
    no_pipe_res_o <= ( x_i[0] + x_i[1] ) +
                     ( x_i[2] + x_i[3] );
  end

// pipeline
logic [7:0] s1 = '0;
logic [7:0] s2 = '0;

always_ff @( posedge clk_i )
  begin
    s1         <= x_i[0] + x_i[1];
    s2         <= x_i[2] + x_i[3];
    pipe_res_o <= s1 + s2;
  end
endmodule




По положительному фронту (так называемый posedge) clk_i на вход модуля были поданы 4 числа: 4, 8, 15 и 23. На следующий положительный фронт в регистре no_pipe_res_o появился ответ 50, а в регистрах s1 и s2 значения полусумм 12 и 38. На следующий posedge в регистре pipe_res_o появился ответ 50, а в no_pipe_res_o появился 0, что не удивительно, т.к. четыре нуля были поданы в качестве входных значений и схема их честно сложила.

Сразу заметно, что результат в pipe_res_o запаздывает на один такт чем в no_pipe_res_o, т.к. были добавлены регистры s1 и s2.

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


Несложно заметить, что обе ветки (с конвейером так и без) готовы к тому, что бы каждый такт принимать новую порцию данных.

Возникает справедливый вопрос: зачем использовать конвейер, если на вычисления тратятся больше времени (тактов) и это занимает больше триггеров (ресурсов)?

Производительность конвейера


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

Существует формула, по которой рассчитывается максимальная допустимая частота между двумя триггерами. Её упрощенный вариант:

Легенда:
  • Fmax — максимальная тактовую частота.
  • Tlogic — задержка при прохождении сигнала через логические элементы.

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


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

Чем больше логических элементов будет на пути сигнала от одного триггера до другого, тем больше будет значение Tlogic. Если мы хотим увеличить частоту, то нам надо сокращать это значение! Как же это сделать?
  1. Оптимизировать код. Иногда новички (да и опытные разработчики, чего греха таить!) пишут такие конструкции, которые превращаются в очень длинные цепочки логики: этого надо избегать, различными трюками. Иногда необходимо оптимизировать схему под конкретную архитектуру FPGA.
  2. Добавить регистр(ы). Просто разрезаем логику на кусочки.

Добавление регистров — это и есть конвейеризация! Конвейеризированные схемы в большинстве случаев могут работать на больших частотах, чем те, которые без конвейера.

Рассмотрим следующий пример:
Есть два триггера, A и B, сигнал между ними преодолевает два «облачка» логики, одно из которых занимает 4 ns, другое 7 ns. Если будет проще, представьте, что зеленое облако — это сумматор, а оранжевое — мультиплексор. Примеры и числа взяты из головы.


Чему будет равна Fmax? Сумма 4 и 7 ns будет отображать значение Tlogic: Fmax ~91 МГц.

Добавим регистр C между комбинационкой:


Fmax оценивается по худшему пути, время которого теперь составляет 7 ns, или ~142 МГц. Разуеется, не надо бросаться и добавлять регистры для повышения частоты где попало, т.к. можно легко напороться на самую частую ошибку (в моем опыте), что где-то на один такт поехала схема, т.к. где-то добавили регистр, а где-то нет. Бывает, схема к конвейризации не была готова, т.к. есть обратная связь, в связи с задержкой на такт(ы), начала неправильно работать.

Подведем небольшой итог:
  1. Благодаря конвейеризации можно увеличить пропускную способность схемы, жертвуя временем обработки и ресурсами.
  2. Разбивать логику необходимо как можно равномерно, т.к. максимальная частота схемы зависит от худшего пути. Разбив 11 ns пополам, можно было бы получить 182 МГц.
  3. Разумеется, до бесконечности увеличивать частоту не получится, т.к. временными параметрами, на которые мы сейчас закрыли глаза, нельзя будет пренебрегать. В первую очередь Trouting.

Отмечу, что иногда цели добиться максимальной частоты нет, чаще всего наоборот: известна частота, к которой надо стремиться. К примеру, стандартная частота работы 10G MAC-ядра — 156.25 МГц. Может оказаться удобным, что бы вся схема работала от этой частоты. Бывают требования, которые напрямую не связаны с тактовой частотой: к примеру, есть задача, что бы какая-то система поиска делала 10 миллионов поисков в секунду. С одной стороны, можно сделать конвейер на частоте 10 МГц, и подготовить схему, чтобы принимать данные каждые такт. С другой, более выгодным вариантом может оказаться такой: повысить частоту до 100 МГц и сделать такой конвейер, который готов принимать данные каждый 10-й такт.

Конечно, этот пример очень детский, но он является ключом к созданию схем с очень большой производительностью.

Советы и личный опыт


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

Использовать сигналы валидности

Иногда начинающие FPGA-разработчики допускают следующую ошибку: определяют факт прихода новых данных по самим данным. К примеру, проверяют данные на ноль: если не ноль, то новые данные пришли, иначе ничего не делаем. Так же вместо нуля используют какую-то другую «запрещенную» комбинацию (все единицы).

Это не очень правильный подход, так как:
  • На зануление и проверку тратятся ресурсы. На больших шинах данных это может быть очень дорогим.
  • Может быть неочевидно для других разработчиков, что здесь происходит (WTF code).
  • Какой-то запрещенной комбинации может и не быть. Как в примере с сумматором выше: все значения от 0 до 255 валидны и могут подаваться на сумматор.

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

Добавим сигналы валидности x_val_i и pipe_res_val_o в пример выше:
Скрытый текст
module pipe_with_val_example(                                                           
  input              clk_i,                                                   
                                   
  input        [7:0] x_i [3:0],
  input              x_val_i,                                                                             
                                                                                                          
  output logic [7:0] pipe_res_o,                                                                          
  output logic       pipe_res_val_o                                                                       
);                                                                                                        
                                                                                             
logic [7:0] s1 = '0;                                                                                      
logic [7:0] s2 = '0;                                                                                      
                                                                                                          
logic       x_val_d1 = 1'b0;                                                                              
logic       x_val_d2 = 1'b0;                                                                              
                                                                                                          
always_ff @( posedge clk_i )                                                                              
  begin                                                                                                   
    x_val_d1 <= x_val_i;                                                                                  
    x_val_d2 <= x_val_d1;                                                                                 
  end                                                                                                     
                                                                                                          
always_ff @( posedge clk_i )                                                                              
  begin                                                                                                   
    s1         <= x_i[0] + x_i[1];                                                                        
    s2         <= x_i[2] + x_i[3];                                                                        
    pipe_res_o <= s1 + s2;                                                                                
  end                                                                                                     
                                                                                                          
assign pipe_res_val_o = x_val_d2;                                                                         
                                                                                                          
endmodule    





Согласитесь, сразу же стало нагляднее, что здесь происходит: в какой такт данные на входе и выходе оказываются валидными?

Пару замечаний:
  • В этот пример было бы неплохо добавить reset, что бы он сбрасывал x_val_d1/d2.
  • Несмотря на то, что данные невалидны, сумматоры их всё равно складывают и кладут данные в регистры. С другой стороны, можно было бы разрешать сохранять в регистры только тогда, когда данные валидны. В этом примере сохранение невалидных данных ни к чему плохому не приводит: я разрешение работы и не добавлял. Однако, если есть необходимость оптимизировать по потребляемой мощности, то придется добавить такие сигналы и попросту ток не гонять :).

Подумать об отправителе и получателе данных

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



Легенда:
  • fifo_0, fifo_1 — фифошки для размещения данных пакета. В этом примере подразумевается, что перед началом чтения из fifo_0 весь пакет уже там размещен, а логика за fifo_1 не требует целого пакета для начала обработки.
  • arb — арбитр, который решает когда вычитывать данные из fifo_0 и подавать на вход модуля A. Вычитывание производится выставлением сигнала rd_req (read request).
  • A, B, C — абстрактные модули, которые образуют конвейерную цепочку. Совершенно не обязательно, что каждый модуль обрабатывает данные один такт. Модули внутри тоже могут быть конвейеризированы. Они выполняют какую-то обработку пакета, к примеру, образуют систему парсеров или проводят модификацию пакета (например, подменяют MAC-адреса источника или получателя).

В этом примере fifo_0 является отправителем данных, а fifo_1 — получателем. Допустим, что по каким-то причинам из fifo_1 не всегда вычитываются данные или скорость чтения данных меньше, чем скорость записи, в итоге, это фифо может переполниться. Не надо писать в полную фифошку, т.к., как минимум, вы сломаете пакет (из пакета может пропасть какой-то набор данных) и некорректно его передадите.

Что fifo может сообщить?
  1. used_words — количество использованных слов. Зная размер фифошки, можно рассчитать количество свободных слов. В этом примере считаем, что одно слово равно одному байту.
  2. full и almost_full — сигналы о том, что фифо уже заполнилось, либо «почти» заполнилось. «Почти» определяется настраиваемой границей использованных слов.

Чтобы не ломались данные, нам надо когда-то не вычитывать данные пакета из fifo_0 и приостанавливать работу конвейера. Как это сделать?

Алгоритм в лоб

Допустим, мы выделили fifo_1 под MTU, к примеру, 1500 байт. Перед началом чтения пакета из fifo_0 arb смотрит на количество свободных байт в fifo_1. Если оно больше, чем размер текущего пакета, то выставляем rd_req и не снимаем до конца пакета. Иначе ждем пока фифо не освободится.

Плюсы:
  • Арбитраж относительно простой и быстро делается.
  • Меньше граничных условий для проверки.

Минусы:
  • fifo_1 надо выделять под MTU. А если у вас MTU 64K, то пара таких фифо отожрет вам пол-FPGA) Иногда без фифо под пакет нельзя обойтись, но лучше ресурсы экономить.
  • Если конвейер простаивает, т.к. места на целый пакет не хватает, то мы теряем в производительности. Если говорить более точно, то худший случай (переполнение fifo_0, в которую тоже кто-то кладет пакеты) наступает быстрее.

Примечание:
При расчете свободного места надо сделать запас на длину конвейера. К примеру, прямо сейчас в fifo_1 свободно 100 байт, к нам пришел пакет 95 байт. Смотрим, что 100 больше 95 и начинаем пересылать пакет. Это не совсем верно, т.к. мы не знаем текущего состояния конвейера — если мы обрабатываем предыдущий пакет, то в худшем случае в fifo_1 дополнительно запишутся еще слова в количестве длины конвейера (в тактах). Если конвейер работает 10 тактов, то еще 10 слов попадут в fifo_1, и фифо может переполниться, когда мы будем писать пакет из 95 байт.

Улучшение «алгоритма в лоб»

Зарезервируем в fifo_1 количество слов равное длине конвеера. Используем сигнал almost_full.
Арбитр каждый такт смотрит на этот сигнал. Если он равен 0, то мы вычитываем одно слово пакета, иначе — нет.

Плюсы:
  • fifo_1 может быть не очень большой, к примеру, на пару десятков слов.

Минусы:
  • Модули A, B, C должны быть готовы к тому, что пакет будет приходить не каждый такт, а по частям. Если более формально: сигнал валидности внутри пакета может прерываться.
  • Необходимо проверить больше граничных условий.
  • Если добавятся новые стадии конвейера (следовательно, увеличиться его длина), то можно забыть уменьшить верхнюю границу в fifo_1. Либо необходимо как-то параметром эту границу высчитывать, что может быть нетривиально.

Остановка конвейера во время работы

У предыдущих вариантов есть общая черта: если слово пакета попало в начало конвейера, то уже ничего не сделать: через константное количество тактов это слово (либо его модификация) попадет в fifo_1.

Альтернативный вариант заключается в том, что конвейер можно останавливать во время работы. У каждой стадии с номером M появляется вход ready, который показывает, готова ли стадия M+1 принимать данные или нет. На вход самой последней стадии заводится инверсия full или almost_full (если есть желание немного перестраховаться).



Плюсы:
  • Можно безболезнено добавлять новые стадии конвейера.
  • У каждой стадии теперь могут появится нюансы в работе, но нет нужды переделывать архитектуру. К примеру: мы захотели добавить в пакет какой-то VLAN-tag, перед тем как положить пакет в fifo_1. VLAN-tag — это 4 байта, в нашем случае — 4 слова. Если пакеты идут друг за другом, то в первых двух случаях arb должен был понять, что надо после конца пакета сделать паузу в 4 такта, т.к. пакет увеличится на 4 слова. В этом случае всё само отрегулируется и модуль, который вставляет VLAN в момент его вставки выставит ready, равным нулю, на стадию конвейера, которая стоит перед ним.

Минусы:
  • Код становится более сложным.
  • При верификации необходимо проверить еще больше граничных условий.
  • Скорее всего занимает больше ресурсов, чем предыдущие варианты.


Использовать стандартизованные интерфейсы

Вышеописанные проблемы можно разрешить используя интерфейсы, в которых уже заложены вспомогательные сигналы. Альтера для потоков данных предлагает использовать интерфейс Avalon Streaming (Avalon-ST). Его подробное описание можно найти тут. Кстати, именно этот интерфейс используется в Альтеровских 10G/40G/100G Ethernet MAC-ядрах.


Сразу обращает на себя внимание наличие сигналов valid и ready, о которых мы говорили ранее. Сигнал empty показывает количество свободных (неиспользуемых) байт в последнем слове пакета. Сигналы startofpacket и endofpacket определяют начало и конец пакета: удобно использовать эти сигналы для сбросов различных счетчиков, которые отсчитывают оффсеты для выцепления нужных данных.

Xilinx предлагает использовать AXI4-Stream для этих целей.



В целом, набор сигналов похож (на этой картинке не все сигналы, возможные доступные в этом стандарте). Если честно, AXI4-Stream я никогда не использовал. Если кто-то использовал оба интерфейса, буду признателен, если поделитесь сравнением и впечатлениями.

Думаю, плюсы использования стандартных интерфейсов упоминать не надо. Ну, и минусы очевидны: необходимо соблюдать стандарт, что выражается в большем количестве кода, ресурсов, тестов и пр. Конечно, можно делать свои велосипеды, но в перспективе на большом проекте (и топовых чипах) это может вылезти боком.

Бонус


В качестве примера посложнее чем пара сумматоров предлагаю сделать модуль, который в Ethernet-пакете меняет местами MAC-адреса источника и получателя (mac_src и mac_dst). Это может полезным, если есть желание весь трафик, который приходит на девайс отправить обратно (так называемый заворот трафика/шлейф или loopback). Реализация, конечно же, должна быть сделана конвейером.

Используем интерфейс Avalon-ST с 64-битной шиной данных (для 10G) без сигналов ready и error. В качестве тестового пакета возьмем тот, который смотрели на xgmii в предыдущей статье. Тогда:
  • mac_dst — 00:21:CE:AA:BB:CC (байты с 0 по 5). Располагается в 0 слове в байтах 7:2.
  • mac_src — 00:22:15:BF:55:62 (байты с 6 по 11). Располагается в 0 слове в байтах 1:0 и в 1 слове в байтах 7:4.


Код на SystemVerilog под спойлером:
Скрытый текст
module loop_l2(

  input                   clk_i,
  input                   rst_i,

  input        [7:0][7:0] data_i,
  input                   startofpacket_i,
  input                   endofpacket_i,
  input        [2:0]      empty_i,
  input                   valid_i,
  
  output logic [7:0][7:0] data_o,
  output logic            startofpacket_o,
  output logic            endofpacket_o,
  output logic [2:0]      empty_o,
  output logic            valid_o

);

logic [7:0][7:0] data_d1;
logic            startofpacket_d1;
logic            endofpacket_d1;
logic [2:0]      empty_d1;
logic            valid_d1;

logic [7:0][7:0] data_d2;

logic [7:0][7:0] new_data_c;

always_ff @( posedge clk_i or posedge rst_i )
  if( rst_i )
    begin
      data_d1          <= '0; 
      startofpacket_d1 <= '0; 
      endofpacket_d1   <= '0; 
      empty_d1         <= '0; 
      valid_d1         <= '0; 
      
      data_o           <= '0; 
      startofpacket_o  <= '0; 
      endofpacket_o    <= '0; 
      empty_o          <= '0; 
      valid_o          <= '0;

      data_d2          <= '0;
    end
  else
    begin
      data_d1          <= data_i;          
      startofpacket_d1 <= startofpacket_i; 
      endofpacket_d1   <= endofpacket_i;   
      empty_d1         <= empty_i;         
      valid_d1         <= valid_i;         
      
      data_o           <= new_data_c;          
      startofpacket_o  <= startofpacket_d1; 
      endofpacket_o    <= endofpacket_d1;   
      empty_o          <= empty_d1;         
      valid_o          <= valid_d1;        

      data_d2          <= data_d1;
    end

always_comb
  begin
    new_data_c = data_d1;

    if( startofpacket_d1 )
      begin
        new_data_c = { data_d1[1:0], data_i[7:4], data_d1[7:6] };
      end
    else
      if( startofpacket_o )
        begin
          new_data_c[7:4] = data_d2[5:2];
        end
  end

endmodule


Не всё так страшно, как могло показаться ранее: большая часть строчек ушла на описание входов/выходов модуля и линии задержки. По startofpacket определяем в каком слове мы находимся, и какие данные надо подставлять в комбинационку new_data_c, которая потом защелкивается в data_o. Сигналы startofpacket, endofpacket, empty, valid задерживаются на нужное количество тактов.

Симуляция модуля:



Как видим, MAC-адреса поменялись местами, а все остальные данные в пакете не изменились.

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

Спасибо за уделенное время и внимание! Если появились вопросы, задавайте без сомнений.

Список литературы


  • Advanced FPGA Design — одна из лучших книг по FPGA, что я видел. В главе 1 подробно разобрана конвейеризация. ИМХО, книга для тех, кто уже получил какой-то опыт в FPGA разработке и хочет систематизировать свои знания либо какие-то интуитивные догадки.
  • TimeQuest Timing Analyzer — приложение для оценки значения таймингов (в том числе Fmax) для FPGA фирмы Altera.

P.S. Благодарю des333 за конструктивную критику и советы.
Иван Шевчук @ishevchuk
карма
82,5
рейтинг 8,8
FPGA Design/Verification Engineer
Похожие публикации
Самое читаемое Разработка

Комментарии (54)

  • +2
    Названо много pros за конвейеризацию, но забыты cons:
    1) Бездумная конвейеризация там где не надо — это лишние задержки и лишний расход ресурсов, иногда значительный.
    2) Алгоритмы с обратными связями (в обработке сигналов, к примеру) очень трудно конвейеризировать — либо просто мучительно больно с технической точки зрения, либо введение задержек изменяет сам алгоритм и сказывается на общей стабильности (качестве, характеристиках) системы.

    А так здорово, ждем еще.
    • 0
      Спасибо) Вот и дискуссия появилась.
      Обратную связь я упомянул)
      Бывает, схема к конвейризации не была готова, т.к. есть обратная связь, в связи с задержкой на такт(ы), начала неправильно работать.

      С ЦОС я не работал: если более развернуто расскажете про проблемы, возникаемые там — будет замечательно)
      • +1
        Можно взять самый банальный пример рекурсивного фильтра x = k*a + (1-k)*x, где a — входные данные, x — выход, k — коэффициент от 0 до 1. На каждом такте в основной петле надо произвести умножение ( (1-k) * x) и сложение с k*a. Для макисмального быстродействия тут надо 3 такта как минимум, которых у нас нет.
        Если вставлять задерки в петлю обратной связи, то характеристика фильтра начнет меняться и в конечном итоге рассыпется.
        В общем алгоритмы с обратными связями (feed back) — зло, feed-forward — наше все! ;)
  • 0
    Давно не занимаюсь ПЛИС, но статься понравилась, хорошо пишете :)
  • 0
    Если честно, AXI4-Stream я никогда не использовал. Если кто-то использовал оба интерфейса, буду признателен, если поделитесь сравнением и впечатлениями.

    Судя по картинкам с временными диаграммами, ничем принципиально они не отличаются: двухфазное рукопожатие + полезная нагрузка. AMBA (включая AXI4) сейчас стала по сути промышленным стандартом, поэтому если будете покупать какой-то готовый IP-блок, то там скорей всего будет AXI.
    • +1
      По этой картинке я тоже принципиальной разницы не увидел, но у AXI4-Stream есть сигнал TUSER, который может быть произвольной длины. По описанию, туда можно положить какую-то информацию о пакете: к примеру, количество vlan'ов, чтобы знать с какого байта начинается IP-заголовок.

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

      Еще есть сигнал TDEST, который явно показывает, куда (на какой порт) надо передать этот пакет, в Avalon-ST этого нет, приходится это добавлять в вышеупомянутую структуру)
      • 0
        Прелесть Avalon-* интерфейсов в том, что перечень сигналов весьма гибок и можно пользоваться только теми, которые нужны, все остальное буде сделано шинной инфраструктурой автоматически. Это следствие того, что avalon «вырос» в мире FPGA.
        Опыт работы с AXI у меня весьма эпизодический, но там, как я понимаю, есть фиксированный перечень сигналов в интерфейсе, который надо реализовывать всегда — из ARM это перешло и сюда.
    • 0
      Четырехфазное рукопожатие, ты хотел сказать
      • 0
        Ага, спасибо что поправили.
        • 0
          Мы теперь на вы? :D
          • 0
            Не, просто не прочёл никнейм. Как тесен мир )
  • 0
    Хочу отметить, что во многих случаях можно обойтись без сигналов валидности. Простой сценарий: данные на вход поступают каждый квант времени, и проводят на каждой ступени конвейера один квант. Тут всё что нужно знать об устройстве в целом — длина конвейера, то есть количество ступеней. Я таким способом генерировал видео.
    • 0
      Конечно, если данные идут всегда, то смысла вводить сигнал валидности особо нет. С другой стороны, его задержанные братья будут явно показывать на какой стадии мы сейчас находимся, что может быть полезно при отладке.

      Насчет «многих случаев»: думаю, всё-таки больше приложений, где данные идут не всегда, следовательно сигнал валидности нужен. Конечно, процент тех и других случаев вряд ли кто-то подсчитывал :)
      • 0
        И всё же, чем больше статики, тем естественнее аппаратное решение, — проще логика, полнее загрузка конвейера.
  • –2
    IMHO
    Использование always_comb, для такого кода в бонусе выдает, что это писал начинавший с программирования софта.
    Железячник для простоты читаемости кода написал бы явный assign, без переприсвоения.
    • 0
      Думаю, дело вкуса. Мои коллеги не против, из минусов этой записи я вижу лишь то, что могут получиться два мультиплексора, которые стоят друг за другом, что не очень хорошо по частотке. Если такое вылезает, то приходится переделывать на case.
      Либо еще есть какие-то минусы от такой записи?

      P.S. Разве FPGA-шник должен быть железячником? :)
    • 0
      Если не затруднит, не могли бы Вы написать более читаемый вариант этого блока.
      Я понимаю, как это можно описать при помощи assign, но тот вариант, который приходит на ум мне, не кажется более читаемым.
      Может быть, Вы имеете в виду что-то другое.
      • +2
        Кстати always в Верилоге довольно часто применяют для описания сложной комбинаторной логики, типа функций переходов автоматов и т.д. Гораздо нагляднее, чем городить?: (?: (?: (?: (? :)))).

        Но вот код автора действительно приведет к куче ворнингов при синтезе, например про неиспользуемые биты в data_d2.

        Лично мне религия не позволяет написать так, как автор — описание очень похоже на inferred latch, если не заметить строку «new_data_c = data_d1;», но думаю, что это уж точно дело вкуса.
        • 0
          Хм, загнал модуль в Квартус, но ни одного ворнинга не увидел.
          • 0
            Synplify Premier 2014.09:

            @W:CL169: loop_l2.sv(30) | Pruning register data_d2[0][7:0]

            @W:CL169: loop_l2.sv(30) | Pruning register data_d2[1][7:0]

            @W:CL169: loop_l2.sv(30) | Pruning register data_d2[6][7:0]

            @W:CL169: loop_l2.sv(30) | Pruning register data_d2[7][7:0]
            • 0
              Ага, закрадывалась мысль, что используете или Synplify, или Vivado.
              Значит, Synplify более паранойный)

              Защелкивание лишних данных мне никогда особо не мешало, но т.к. есть мысли переехать на Synplify, то, возможно, придется это учесть, спасибо!

              Если не секрет, в своих проектах Вы от всех предупреждений избавляетесь?
              • 0
                Я сейчас сам не пишу код. Но поскольку наша контора продает RTL тем, кто делает ASIC-и, то к предупреждениям отношение очень серьезное. Наши индусы используют статический анализатор LEDA с нашим собственным набором правил (вроде бы, переделанным из IBM-овского), и релиз не пропустят, если есть хоть одно предупреждение. Если есть какие-то false-positive предупреждения, то они индивидуально маскируются.
        • 0
          Гораздо нагляднее, чем городить?: (?: (?: (?: (? :)))).

          Для такого случая наглядней assign, тк есть переприсвание в начале на неупомянутые в if биты.
          Это затрудняет понимание именно этого кода.
          Куда понятней тот же код, где явно описано три возможных вариант в столбик и где (главное для понимания) определены все биты:
          assign new_data_c = ()? {,} : ()? {,} : {,};
      • 0
        Я обратил внимание, именно, на сложность интерпретации переприсваивания и возможных ошибок при этом.
        Переприсваивание это строчка new_data_c = data_d1; вначале.
        assign более явно описывает комбинаторную логику и сразу понятна ее реализация.
        Там нельзя сделать переприсваивание и ошибки, только 3 явных и главное полных состояния выхода.
        • 0
          Я прекрасно понимаю, что это может быть неинтуитивно для тех, кто так не писал :)
          Возможную проблему с латчем, если кто-то удалит первую строчку я тоже знаю. Никаких нарушений канонов либо устоявшихся рекомендаций (например, смешивание блокирующих и неблокирующих присваиваний в одном always-блоке) в таком написании я не вижу.

          Раньше я писал через assign как раз так, как Вы предлагаете, но у меня очень часто возникает задача в длинном слове поменять часть байт и через assign и кучу конкатенаций мне не нравилось делать, т.к. пишется много кода. Больше кода — больше ошибок: можно не те биты заменить, либо на один промазать, читаемость хуже…

          Этот прием мне пришел в голову, когда я понял, что намного проще сначала присвоить, грубо говоря, default value, задержанное на несколько тактов, а потом уже его модифицировать, подменяя только нужные байты. Такая подмена это более высокоуровневое написание, чем assign, следовательно, должна быть более интуитивным и понятным. Думаю, дело вкуса и не более.
          • 0
            А как-нибудь так?

            if ( startofpacket_d1 )
                new_data_c = { data_d1[1:0], data_i[7:4], data_d1[7:6] };
            else if ( startofpacket_o )
                new_data_c = {data_d2[5:2], data_d1[3:0]};
            else
                new_data_c = data_d1;     
            
            • 0
              .
            • 0
              Можно и так, но добавилась «лишняя» информация в ввиде data_d1[3:0]. :) Большой разницы через assign либо через так как Вы предложили я не вижу.

              Мне приходится со структурами похожую операцию проделывать:
              typedef struct packed {
                logic [7:0] a;
                logic [7:0] b;
                logic [7:0] c;
              } s_t;
              
              s_t s_comb;
              s_t s_input;
              s_t s_delayed; // задержанная на несколько тактов s_input
              
              // подменяем в s_delayed только поле c
              always_comb
                begin
                  s_comb = s_delayed;
              
                  if( some_flag )
                    s_comb.c = 8'd42;
                end
              


              Как это красиво переписать без первой строчки?
              • 0
                SystemVerilog вас до добра не доведет :D

                На самом деле, я видел код «с первой строчкой» и в чистом Верилоге — стандарту он не противоречит, а все остальное — дело привычки. LEDA на такое не ругается :)
                • 0
                  SystemVerilog реально крутая вещь)
                  Для меня не очень понятны советы новичкам, которые спрашивают «VHDL или Verilog?», и ему отвечают Verilog, мол он проще и похож на С. Должен быть спор VHDL vs SystemVerilog) Сейчас всё быстро меняется, и если надо разрабатывать большие проекты, то там только SystemVerilog ИМХО. (Хотя от приложения зависит, может если тим DSP, то тех извращений, что я делаю с данными, там не надо делать)

                  Правда, насколько я знаю, есть проблема в том, что не все синтезаторы поддерживают, но Synplify вроде впереди планеты всей и всё поддерживает)
            • 0
              А сейчас это полный аналог assign. (если поставить неблокирующее присвоение иначе приоритет будет неправильный)
              И тут как раз кому как нравится визуально.

              • 0
                Разумеется это полный аналог assign, так как код делает то же самое. Я тоже предпочитаю assign, чтобы меньше букв писать, но когда логика на два экрана то assign уже не очень подходит.
          • 0
            Удивлен, что раньше использовали assign, но аргументация такого перехода понятна.
            Для себя, если придется править, то может и быстрее. Но вот если передавать код, то другому разобраться в нем сложнее.
            Просто всегда встречал такой код у тех, кто из программирования пришел к написанию RTL.
    • +1
      Единственное, для большей компактности можно было бы убрать begin/end вокруг однострочных блоков.
      А так, на мой взгляд, более читаемое описание тут вряд ли получится.
  • +1
    Что касается стандарта AXI4-Stream (а также AXI4 и AXI4-Lite), то они в целом довольно удобные для использования, с некоторыми исключениями.

    Для управления процессом передачи данных используется два сигнала — TVALID и TREADY. TVALID поступает от источника и сигнализирует готовность данных на шине, TREADY поступает от приемника и подтверждает прием.

    Если приемник уверен, что сможет сразу принять хотя бы одно слово данных от источника — то он может выставить изначально TREADY=1. В этом случае передача одного слова занимает один такт. Если же приемник не уверен (допустим, способность сразу принять данные зависит от их адреса) — то он должен выставить исходно TREADY=0, и тогда передача данных продлится минимум два такта. Стандарт запрещает комбинационные (безрегистровые) связи между сигналами TVALID и TREADY, поэтому нельзя сначала проверить адрес и потом уже решить, могут ли данные быть приняты в этом же такте.

    В AXI4 (которая с адресами) есть еще такая проблема, что при передаче пакетов (bursts) нужно заранее знать длину пакета и выставить ее на шину. Нельзя передавать пакеты с неизвестной заранее длиной. Вернее сказать, попытаться-то можно, но приемник не обязан принимать данные (может выставить TREADY=0) до тех пор, пока ему не будет предоставлена информация о длине. Такое соглашение приводит к необходимости применения лишних буферов и логики там, где без них можно было бы обойтись.

    Еще одна проблема, присутствующая в AXI4 и AXI4-Lite — это то, что ведущее устройство (master) может запросить данные из ведомого (slave) и при этом не быть готово к их приему. Допустим, процессор считывает содержимое одного из регистров периферийного устройства. Как правило, такие регистры подключаются к шине данных через мультиплексор, который выбирает нужный регистр в зависимости от адреса. Так вот, к тому моменту, когда процессор будет готов принять данные, он может уже снять адрес с шины адреса. Поэтому приходится либо запоминать адрес и держать его на мультиплексоре, либо запоминать считанные данные после мультиплексора — ставить лишний регистр. Ресурсы, ресурсы, такты…
    • 0
      Все это мелочи по сравнению с тем, что AXI может вернуть данные не в том порядке, в котором их попросил мастер.
      • 0
        Ну, это по желанию. В разделе А5.1 написано: «All transactions with a given AXI ID value must remain ordered, but there is no restriction on the ordering of transactions with different ID values». Эта же информация дублируется в разделе A6 во многих местах. Так что, если ведущее устройство хочет, чтобы все транзакции исполнялись в порядке поступления запросов, он может этого потребовать от шины и ведомых устройств.
        • 0
          Здесь есть один нюанс. В спецификации AXI сказано: «There are no ordering restrictions between read and write transactions with the same AWID and ARID. If a master requires an ordering restriction then it must ensure that the first transaction is fully completed before the second transaction is issued». (пункт 8.1 для AXI3 и 8.2 для AXI4),

          Таким образом, действительно мастер может потребовать, чтобы все запросы на запись выполнялись по порядку, и все запросы на чтение тоже, но потребовать, чтобы запросы на запись и чтение не переупорядочивались между собой, он не может, даже если ARID и AWID будут одинаковы.
          • 0
            Объясните мне пожалуйста, зачем вникать в эти ньюансы работы AXI шины (как будто других нет), если для работы с AXI шиной Xilinx предоставляет кучу вспомогательных IP ядер: AXI DMA, Datamover, AXI Master Interface и тд?
  • 0
    Спасибо! Статья очень кстати. Как раз сейчас столкнулся с проблемами в работе своего проекта в железе. Похоже, что как раз «облако логики» не успевает. Есть о чем подумать.
    И совсем не понял вот этот момент:

    По положительному фронту (так называемый posedge) clk_i на вход модуля были поданы 4 числа: 4, 8, 15 и 23. На следующий положительный фронт в регистре no_pipe_res_o появился ответ 50

    Почему так? Я вот ошибочно считал, что регистр будет запоминать данные по переднему фронту и проблема в том, что фронт клок придет раньше, чем установится состояние сумматоров, что приведет к тому, что запишется не то значение. Читаю про D-триггер, там речь о том, что по после переднего фронта С он начинает заносить значение в первый Т триггер и только после падения С уже подается на выход то, что там установилось. У вас же написано (и показано на картинках), что данные на выход попадут только по следующему переднему фронту CLK. Почему так?
    • 0
      Здесь есть тонкий момент, о котором я не написал, но подразумевал.
      Считается, что схема полностью синхронная: вход x_i является выходом какого-то триггера, который тоже работает по clk_i. В первый posedge данные защелкнулись в триггер x_i, и стали валидны на его выходе. Весь такт сумматоры считают свои значения и на следующий положительный фронт сумма защелкнется в no_pipe_res_o.

      Читаю про D-триггер, там речь о том, что по переднему фронту С он заносит значение в первый Т триггер и после падения С уже подается на выход.

      Если честно, такого никогда не видел…
      • 0
        Возьмем такой пример:
        module some_example(
          input              clk_i,
        
          input        [7:0] a_i,
          input        [7:0] b_i,
        
          output logic [7:0] no_pipe_res_o
        
        );
        
        logic [7:0] a_sync;
        logic [7:0] b_sync;
        logic [7:0] c;
        
        always_ff @( posedge clk_i )
          begin
            a_sync <= a_i;
            b_sync <= b_i;
          end
        
        assign c = a_sync + b_sync;
        
        always_ff @( posedge clk_i )
          begin
            no_pipe_res_o <= c;
          end
        endmodule
        


        Я специально сигналы a и b пересохраняю в триггер, что бы с триггера отдались данные на сумматоры.

        Так выглядит функциональная симуляция в Квартусе (кстати 13):



        Вот так временная:


        Теперь представьте, что a/b_sync вынесены из этого модуля и выходы этих триггеров подаются на a_i, b_i в таком примере:

        module some_example_2(
          input              clk_i,
        
          input        [7:0] a_i,
          input        [7:0] b_i,
        
          output logic [7:0] no_pipe_res_o
        
        );
        
        always_ff @( posedge clk_i )
          begin
            no_pipe_res_o <= a_i + b_i;
          end
        endmodule
        


        По поведению ничего не поменяется)
      • 0
        Немного адаптировал под Icarus
        no_pipe_example.v
        module no_pipe_example(
          input              clk_i,
        
          input        [7:0] x_1,
          input        [7:0] x_2,
          input        [7:0] x_3,
          input        [7:0] x_4,
        
          output logic [7:0] no_pipe_res_o
        );
        // no pipeline
        always @( posedge clk_i )
            no_pipe_res_o <= ( x_1 + x_2 ) + ( x_3 + x_4 );
        endmodule
        


        У меня в IcarusVerilog получилось так, что данные попадают на выход вообще сразу по переднему фронту.



        Или мы вообще о разных вещах говорим? Verilog и SystemVerilog — ваш пример на чем? Что значит always_ff @( posedge clk_i )?
        • 0
          Мои все примеры на SystemVerilog)

          always_ff @( posedge clk_i ) в этом примере эквивалентен always @( posedge clk_i ).
          Попробуйте пример из сообщения чуть выше с sync подать в симуляцию, подав данные не по положительному фронту)
          Какой конструкцией подаете сигналы x1-4 в Вашем примере?
          • 0
            Мне кажется, что вы подаете данные на tb конструкцией вида
            initial
              begin
                #100;
                x_1 = 4;
                x_2 = 8;
                ...
              end
            


            Попробуйте вот так:
            initial
              begin
                @( posedge tb_clk )
                x_1 <= 4;
                x_2 <= 8;
                ...
              end
            
            • 0
              А какой смысл так писать тестовые модули? Они должны выдавать сигналы во времени именно так, как у меня на графике, так, как данные в реальном времени подаются на вход модулей, а не промежуточных регистров. Если тестовый модуль писать тоже с posedge и в нем значения записывать через <=, то логично, то там может появиться еще один такт. Но зачем так делать?
              По факту получается, что в статье есть 1) текст модулей 2) рисунок входных и выходных сигналов, который не соответствует действительности (вывод данных задерживается на такт у no_piped модуля и на 2 такта у piped), потому что входные сигналы подаются тоже через регистры, но о том, что они есть и как в них пишутся данные — мы не знаем. Надо как-то этот вопрос осветить, наверное.
              • 0
                Смысл заключается в том, что бы сделать так, что бы эти сигналы были синхронны с клоком.
                Попробуйте следующий пример:
                Модуль 1:
                module gen_x(
                  input clk_i,
                  input rst_i,
                  
                  output logic [7:0] x_1_o,
                  output logic [7:0] x_2_o,
                  output logic [7:0] x_3_o,
                  output logic [7:0] x_4_o
                
                );
                
                always @( posedge clk_i or posedge rst_i )
                  if( rst_i )
                    begin
                      x_1_o <= '0;
                      x_2_o <= '0;
                      x_3_o <= '0;
                      x_4_o <= '0;
                    end
                  else
                    begin
                      x_1_o <= x_1_o + 8'd1;
                      x_2_o <= x_2_o + 8'd3;
                      x_3_o <= x_3_o + 8'd5;
                      x_4_o <= x_4_o + 8'd7;
                    end
                endmodule
                


                Модуль 2:
                module top(
                  input clk_i,
                  input rst_i,
                  
                  output logic [7:0] no_pipe_res_o
                );
                  
                logic [7:0] x_1_w;
                logic [7:0] x_2_w;
                logic [7:0] x_3_w;
                logic [7:0] x_4_w;
                
                gen_x gen_x(
                  .clk_i ( clk_i ),
                  .rst_i ( rst_i ),
                
                  .x_1_o ( x_1_w ),
                  .x_2_o ( x_2_w ),
                  .x_3_o ( x_3_w ),
                  .x_4_o ( x_4_w )
                );
                
                no_pipe_example no_pipe(
                  .clk_i ( clk_i )
                
                  .x_1  (  x_1_w ),
                  .x_2   ( x_2_w ),
                  .x_3   ( x_3_w ),
                  .x_4   ( x_4_w ),
                
                  .no_pipe_res_o ( no_pipe_res_o )
                );
                
                


                Просто просимулируйте и скиньте сюда картинку) В этом примере x_1-4 идут с триггеров.
                • 0
                  Просимулирую, но уже позже (
                  А в реальной жизни сигналы синхронны с клоком?
                  • 0
                    Зависит от того, что Вы вкладываете в слово «реальная жизнь».
                    В симуляторе мы отсматриваем модель того, что происходит в жизни. Есть модель функциональная, а есть временная. Еще можно симулировать не RTL-код, а нетлист или гейты, и тогда еще более реальная жизнь происходит. Когда мы выбираем какой-то тип симуляции, то соглашаемся с теми допущениями, что в ней есть. Все эти картинки, это лишь то, как симулятор видит нашу схему на тех входных воздействиях, что в нее подали)
                    Разницу между функциональной и временной можно увидеть выше.

                    К сожалению, из литературы по верификации мне ничего не приходит в голову хорошого и простого. Есть «SystemVerilog for Verification», там есть глава Connecting Testbench and Design, но там чисто SystemVerilog'овские штуки раскрываются и может быть всё усложнено. Посмотрите примеры на testbench.in / asic-world.com, например, www.asic-world.com/examples/verilog/arbiter.html
                  • 0
                    В реальной жизни это зависит от сигналов. Если сигнал идет от АЦП с синхронным интерфейсом, который вы сами тактируете, то они будут синхронными. Если сигнал идет с кнопок, или с UART-а, или с другого устройства, которое работает на своей собственной частоте, независимой от частоты ПЛИС, то они не будут синхронными. В этом случае возможен вариант, когда входные сигналы изменятся слишком близко ко фронту клока в ПЛИС, что приведет к нарушению времени предустановки триггеров (setup violation). С этим можно бороться путем добавления синхронизаторов
                  • 0
                    Чтобы была видна разница, я решил три теста одновременно показать в симуляторе:
                    test0 — данные идут с триггеров gen_x.
                    test1 — данные по @( posedge clk ) делаются.
                    test2 — данные по #20 выставляются.

                    Код:
                    Скрытый текст
                    module top_tb;
                    
                    logic clk;
                    logic rst;
                    
                    initial
                      begin
                        clk = 1'b0;
                        forever 
                          begin
                            #10;
                            clk = !clk;
                          end
                      end
                    
                    initial
                      begin
                        rst = 1'b0;
                        
                        #2;
                        rst <= 1'b1;
                    
                        #2;
                        rst <= 1'b0;
                      end
                    
                    // ********** TEST 0 ***********
                    logic [7:0] test0_res_w;
                    
                    logic [7:0] test0_x_1_w;
                    logic [7:0] test0_x_2_w;
                    logic [7:0] test0_x_3_w;
                    logic [7:0] test0_x_4_w;
                    
                    gen_x gen_x(
                      .clk_i                                  ( clk               ),
                      .rst_i                                  ( rst               ),
                    
                      .x_1_o                                  ( test0_x_1_w       ),
                      .x_2_o                                  ( test0_x_2_w       ),
                      .x_3_o                                  ( test0_x_3_w       ),
                      .x_4_o                                  ( test0_x_4_w       )
                    );
                    
                    no_pipe_example no_pe_0(
                      .clk_i                                  ( clk               ),
                    
                      .x_1                                    ( test0_x_1_w       ),
                      .x_2                                    ( test0_x_2_w       ),
                      .x_3                                    ( test0_x_3_w       ),
                      .x_4                                    ( test0_x_4_w       ),
                    
                      .no_pipe_res_o                          ( test0_res_w       )
                    );
                    
                    // ********** TEST 1 ***********
                    logic [7:0] test1_res_w;
                    
                    logic [7:0] test1_x_1_w;
                    logic [7:0] test1_x_2_w;
                    logic [7:0] test1_x_3_w;
                    logic [7:0] test1_x_4_w;
                    
                    initial
                      begin
                        test1_x_1_w = '0; 
                        test1_x_2_w = '0;
                        test1_x_3_w = '0;
                        test1_x_4_w = '0;
                        
                        for( int i = 1; i < 10; i++ )
                          begin
                            @( posedge clk );
                            test1_x_1_w <= i*1; 
                            test1_x_2_w <= i*3;
                            test1_x_3_w <= i*5;
                            test1_x_4_w <= i*7;
                          end
                      end
                    
                    no_pipe_example no_pe_1(
                      .clk_i                                  ( clk               ),
                    
                      .x_1                                    ( test1_x_1_w       ),
                      .x_2                                    ( test1_x_2_w       ),
                      .x_3                                    ( test1_x_3_w       ),
                      .x_4                                    ( test1_x_4_w       ),
                    
                      .no_pipe_res_o                          ( test1_res_w       )
                    );
                    
                    // ********** TEST 2 ***********
                    logic [7:0] test2_res_w;
                    
                    logic [7:0] test2_x_1_w;
                    logic [7:0] test2_x_2_w;
                    logic [7:0] test2_x_3_w;
                    logic [7:0] test2_x_4_w;
                    
                    initial
                      begin
                        test2_x_1_w = '0; 
                        test2_x_2_w = '0;
                        test2_x_3_w = '0;
                        test2_x_4_w = '0;
                        
                        #10;
                        for( int i = 1; i < 10; i++ )
                          begin
                            test2_x_1_w = i*1; 
                            test2_x_2_w = i*3;
                            test2_x_3_w = i*5;
                            test2_x_4_w = i*7;
                            #20;
                          end
                      end
                    
                    no_pipe_example no_pe_2(
                      .clk_i                                  ( clk               ),
                    
                      .x_1                                    ( test2_x_1_w       ),
                      .x_2                                    ( test2_x_2_w       ),
                      .x_3                                    ( test2_x_3_w       ),
                      .x_4                                    ( test2_x_4_w       ),
                    
                      .no_pipe_res_o                          ( test2_res_w       )
                    );
                    endmodule
                    



                    Если что, no_pipe_example — тот, который под Icarus «адаптирован».

                    А вот и симуляция:
                    Скрытый текст



                    Несмотря на то, что по картинке test0_x*, test1_x*, test2_x* полностью идентичны, симулятор их совершенно по разному воспринимает: test0 и test1 полностью идентичны, а test2 выполняет всё на такт раньше)
          • 0
            Мои все примеры на SystemVerilog)
            А в чем разница, если в двух словах? Я вот только с приведением типов столкнулся.

            Тестовый такой:
            testbench.v
            module testbench();
            
            reg tb_clk;
            
            reg [7:0] x_1,x_2,x_3,x_4;
            
            wire [7:0] res, res_pipe;
            no_pipe_example ex1(tb_clk,x_1,x_2,x_3,x_4,res);
            pipe_example    ex2(tb_clk,x_1,x_2,x_3,x_4,res_pipe);
            
            initial
            begin
                $dumpfile("bench.vcd");
                $dumpvars(0,testbench);
            	  tb_clk = 0;
            	
                #50;
                x_1 = 4;
                x_2 = 8;
                x_3 = 15;
                x_4 = 23;
                #100;
                tb_clk = 1;
                #100;
                tb_clk = 0; 
                #100;
                x_1 = 0;
                x_2 = 0;
                x_3 = 0;
                x_4 = 0;
                tb_clk = 1;
                #100;
                tb_clk = 0; 
                #100;
                tb_clk = 1;
                #100;
                tb_clk = 0; 
            end
            
            initial begin
            
            end
            
            
            endmodule
            
            pipe_example.v
            module pipe_example(
              input              clk_i,
            
              input        [7:0] x_1,
              input        [7:0] x_2,
              input        [7:0] x_3,
              input        [7:0] x_4,
            
              output logic [7:0] pipe_res_o
            );
            // pipeline
            logic [7:0] s1 = 0;
            logic [7:0] s2 = 0;
            
            always @( posedge clk_i )
              begin
                s1 <= ( x_1 + x_2 );
                s2 <= ( x_3 + x_4 );
                pipe_res_o <= s1 + s2;
              end
            endmodule
            

            Получается вот что


            Отставание на 1 такт piped модуля вопросов как раз и не вызывает. Суть моих «непоняток» как раз в том, почему данные на выходе no_pipelined модуля со второго такта? Видать такой тест или что-то еще есть в проекте, о чем в тексте не указано. Это ж запутывает, во всяком случае меня :)
            • 0
              SystemVerilog это продолжение стандарта Verilog.
              Туда добавили кучу новых плюшек, как синтезируемых, так и не синтезируемых, которые упрощают и убыстряют разработку.

              Я предлагаю вынести создание клока в отдельный initial, что бы он не мешался.
              В вашем примере вы явно не сообщаете симулятору, что x_* это сигналы, которые привязаны (либо синхронны) с tb_clk. Для того, что бы это указать можно использовать @( posedge tb_clk ) или сделать clocking block и писать что-то типа @cb.

              Попробуйте так:
              initial
                begin
                  tb_clk = 1'b0;
                  forever
                    begin
                       #100;
                       tb_clk = !tb_clk;
                    end
                end
              
              initial
                begin
                   @( posedge tb_clk );
                   x_1 <= 4;
                   x_2 <= 8;
                   x_3 <= 15;
                   x_4 <= 23;
              
                   @( posedge tb_clk );
                   x_1 <= 0;
                   x_2 <= 0;
                   x_3 <= 0;
                   x_4 <= 0;
                end
              
              
              • 0
                Теперь стало похоже. Какие я должен сделать из этого выводы? От куда берется эта задержка в 1 такт. Не понимаю

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.