Я подумал, что напишу объяснение конвейерной обработки для ПЛИС и почему это так важно.

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

entity my_adder is
    Port (
        clk : in std_logic;
        data1 : in std_logic_vector(63 downto 0);
        data2 : in std_logic_vector(63 downto 0);
        in_enabled : in std_logic;
        data_out : out std_logic_vector(64 downto 0);
        out_enabled : out std_logic);
end entity;
architecture foo of my_adder is 
begin
    process(clk)
    begin
        if (rising_edge(clk)) then
            data_out <= std_logic_vector(unsigned("0" & data1) + unsigned("0" + data2)); -- result one bit bigger to account for overflows
            out_enabled <= in_enabled;
    end process;
end architecture;

и вы получите правильный ответ. Проблема в том, что программное обеспечение для синтеза обещает сделать это за один такт. Это означает, что если вы хотите иметь тактовую частоту 100 МГц для своего проекта, то программа синтеза скажет: «Это невозможно. Если вы ожидаете, что я сделаю это добавление за один такт, вам придется сильно уменьшить частоту».

Так что ты можешь сделать? Кроме того, вы можете разбить вычисление на множество маленьких фрагментов и выполнять эти фрагменты одновременно. Вы можете выполнить сложение 16-битными фрагментами, а затем объединить эти фрагменты, чтобы получить результат.

Вы можете обрабатывать эти 4 фрагмента по 16 бит одновременно. Однако у вас еще нет правильного ответа, потому что одна из сумм фрагментов (или даже все из них) может переполниться, и мы не знаем, будет ли переполнена сумма фрагментов, пока не проверим, не переполнится ли сумма фрагментов до того, как она переполнится. . Допустим, мы используем первый тактовый цикл, чтобы сложить эти 4 16-битных фрагмента. В конце первого тактового цикла нижние 16 бит ответа будут правильными. Тогда во втором такте мы можем при необходимости добавить единицу к чанксумме второй с конца. Теперь мы знаем нижние 32 бита ответа. Это будет происходить, пока добавляются 4 16-битных фрагмента для входных данных, которые поступили на один такт позже. Затем в следующем такте мы можем сделать то же самое для второго от начала фрагмента, и, наконец, в четвертом такте мы сможем вывести правильный результат.

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

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

Итак, давайте объявим массив из 4 записей. Каждая запись должна быть достаточно большой, чтобы содержать все необходимое нам состояние. Мы назовем это нашим конвейером. Все состояние, которое у нас есть для сложения, — это 2 входа, 4 суммы чанков, пространство для сигнала data_enabled и место для сборки результата. В результате получается следующая структура:

constant NUM_CHUNKS : integer := 4;
constant PIPELINE_LENGTH : integer := 4;
type chunksums_type is array(NUM_CHUNKS-1 downto 0) of unsigned(16 downto 0);
type state_type is record
    data1 : std_logic_vector(63 downto 0);
    data2 : std_logic_vector(63 downto 0);
    chunksums : chunksums_type;
    result : unsigned(64 downto 0);
    data_enabled : std_logic;
end record state_type;
type pipeline_type is array(PIPELINE_LENGTH-1 downto 0) of state_type;
signal pipeline : pipeline_type;

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

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

Еще одна вещь, которую следует отметить, это то, что на самом деле нам не нужны все кусковые суммы все время. Первая чанксумма может быть помещена непосредственно в результат, поэтому нам не нужно хранить ее где-либо еще. Кроме того, вторую чанксумму можно отбросить после второго такта, потому что она уже была вставлена ​​в результат и больше мы ее использовать не будем. Нам нужно только место, чтобы хранить его в течение одного такта. Точно так же третья сумма фрагментов может быть отброшена после третьего такта. Четвертую часть суммы можно отбросить после четвертого такта, потому что в этом такте она будет вставлена ​​в результат.

В результате получается следующая архитектура:

architecture foo of my_adder is
    type pipeline_type is array(3 downto 0) of unsigned(64 downto 0);
    signal pipeline : pipeline_type := (others => (others => '0'));
    signal part10, part20, part21, part30, part31, part32 : unsigned(16 downto 0) := (others => '0');
    signal out_en_buffer : std_logic_vector(3 downto 0) := (others => '0');
begin
    process(clk)
    begin
        if (rising_edge(clk)) then
            out_enabled <= out_en_buffer(3);
            data_out <= pipeline(3);
        end if;
    end process;
    process(clk)
    begin
        if (rising_edge(clk)) then
            part10 <= unsigned("0" & data1(31 downto 16)) + unsigned("0" & data2(31 downto 16));
            part20 <= unsigned("0" & data1(47 downto 32)) + unsigned("0" & data2(47 downto 32));
            part30 <= unsigned("0" & data1(63 downto 48)) + unsigned("0" & data2(63 downto 48));
            part21 <= part20;
            part31 <= part20;
            part32 <= part31;
            pipeline(0)(16 downto 0) <= unsigned("0" & data1(15 downto 0)) + unsigned("0" & data2(15 downto 0));
            pipeline(1)(15 downto 0) <= pipeline(0)(15 downto 0);
            pipeline(1)(32 downto 16) <= pipeline(0)(32 downto 16) + part10;
            pipeline(2)(31 downto 0) <= pipeline(1)(31 downto 0);
            pipeline(2)(48 downto 32) <= pipeline(1)(48 downto 32) + part21;
            pipeline(3)(47 downto 0) <= pipeline(2)(47 downto 0);
            pipeline(3)(64 downto 48) <= pipeline(2)(64 downto 48) + part32;
            out_en_buffer <= out_en_buffer(2 downto 0) & in_enabled;
        end if;
    end process;
end architecture;

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

Надеюсь кому-нибудь это пригодится :)

Если кому-то интересно, я рассчитал время для двух разных проектов, добавив ограничение по часам, сгенерировав отчет о времени в разделе «Синтез», а затем просмотрев неограниченные пути. Вот наихудшие пути синхронизации для двух разных конструкций (я уменьшил размер data_in до 32 бит, потому что в моей FPGA недостаточно входных и выходных контактов для 64-битных входов):

А вот худший внутренний путь от одного такта к другому для конвейерной схемы (у конструкции без конвейерной обработки нет внутренних путей от одного такта к другому). Это показывает, что мы могли бы запустить внутренние пути почти на 500 МГц :)

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