SEC-Сумматор с SIPOPISO на ∀ количество бит

Привет, хабровы плисоводы!

Пока одни пытаются учить других как надо что-то там делать на ПЛИСах, я продолжаю постигать дзен в имплементации никому ненужных идей не несущих какой-либо практической ценности. Я уже делал и сумматор с одним уровнем логики, и рисовал картины-на-кристалле видаде, и делал не нормальное проектирование в вивадском ECO флоу (txt, видео), и даже делал Трахтенберга на ПЛИСах.

Сегодня мы продолжим путь издевательства над нашей дорогой областью программируемой логики и попробуем што-то новенькое:, а именно мы перевернем типичную фразу «Да у нас в плис все параллельно» и сделаем последовательный сумматор на одном Full Adder, но который может складывать числа любой положительной разрядности ну на оооочень высокой тактовой частоте доступной простой смертной логике.

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

В общем хватит прелюдий.

Цель:

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

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

  1. Реализовать:

    1. модуль parallel-input serial-output для загрузки операндов в вычислительное ядро

    2. вычислительное ядро на базе Full Adder (полного сумматора)

    3. модуль serial-input parallel-output для выгрузки результата

  2. Сделать сигнал загрузки и валидности данных

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

  4. Прототип проекта выполнить для кристалла xc7a35tcsg324–1

Начнем со структурной схемы проекта, приведенной на рисунке ниже.

1a37ff381c9bdabc02b542cce2be9903.png

Схема проекта в целом простая, особых пояснений не требует: приходит вектор входных данных на PSIO, потом преобразуется в последовательность бит (сериализация, младший бит выходит первым), биты поступают на вход полного сумматора FA (выхода два — сумма и знак переноса), выход сумматора идет на вход SIPO для десериализации.

Далее углубляемся по уровню представления ниже и опишем сигналы блоков и доп модули.

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

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

Сигнал записи для SIPO сформируем из сигнала iload и его задержанной копии. Сигнал выставляется в 1, когда приходит iload и сбрасывается в 0, когда приходит задержанная копия iload. Грубо говоря вот такая схема (из элеборейта взята)

flowcontrol

`timescale 1ns / 1ps

module flowcontrol(
    input iclk,
        
    input iload,
    input iload_delayed,
    output oread
);
    
    reg read;
    
    always @(posedge iclk) begin
        if (iload) begin
            read <= 1'b1;
        end
        
        if (iload_delayed) begin
            read <= 1'b0;
        end
    end
 
    assign oread = read;
endmodule

a26554a1077a3eeacbcbdce688fa0842.png

Формировать сигнал валидности ovalid будем из сигнала записи в SIPO (он же clock enable или ice), используя edge detector. Схема оч простая и описывается в пару строчек кода.

Картинка с интернета

Картинка с интернета

Просто берем сигнал и его задержанную на такт копию и в зависимости от того нужно задетектить фронт или спад сигнала, реализуем одну из двух приведенных схем. В этом проекте требуется сформировать сигнал валидности после окончания записи бит в SIPO, поэтому выберем negedge detection

negedge detection waveform

negedge detection waveform

always @(posedge iclk)  
    ce <= ice;

assign ovalid = ((ice == 1'b0) && (ce))? 1'b1 : 1'b0;

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

Картинка из одной моей прошлой статьи про сумматор

здесь мы видим, што это цепочка однотипных элементов fulladder, соединенная цепью переноса oc[x]. И в наша основная задача, превратить эту цепочку в «закольцованный» fulladder. Что-то типа такого:

ecec3712401c8656b849ca24956bc963.png

Основной затык тут в том, чтобы выровнять сигнал переноса и приход данных, а так же сброс переноса при поступлении новых данных. Но оказалось эти нюансы просто решаются добавлением триггеров со сбросом на входы ia, ib, ic. Как таковая схема не претерпела каких-то серьезных изменений, а сам код модуля тоже остался максимально простым и понятным

Полный сумматор с закольцованной цепью переноса

`timescale 1ns / 1ps

module full_adder(
    input iclk,
    input ireset,
    input ia,
    input ib,
    output os,
    output oc
    );
    
    reg a, b, c;
    wire carry;
    
    always @(posedge iclk) begin
        if (ireset) begin
            a <= 0;
            b <= 0;
            c <= 0;
        end else begin
            a <= ia;
            b <= ib;
            c <= carry;
        end
    end
    
    assign {carry, os} = a + b + c;
    assign oc = carry;
    
    
endmodule

Ну и в конце описания блоков, приведу код для стандартных модулей PISO и SIPO.
Важный нюанс тут состоит в том, что нам нужно выдавать младший бит первым. Так же модель PISO имеет выход валидности данных, сформированный как задержанный на так сигнал чтения этих данных (он же clock enable или ice).

piso.v

`timescale 1ns / 1ps

module piso #(
    parameter C_WIDTH = 4)
(
    input iclk,
    input iload,
    input [C_WIDTH - 1 : 0] id,
    input ice,
    output ovalid,
    output oq
    );
    reg valid;
    reg [C_WIDTH-1:0] temp = 0;
    reg q;
    always @(posedge iclk) begin
            
        valid <= ice;
            
        if(iload) begin
            temp <=id;
               q <= 0;
        end else begin
            if (ice) begin
                  q <= temp[0];
               temp <= temp>>1;
            end                   
        end
    end

    assign oq     =     q;
    assign ovalid = valid;
    
endmodule

Для модуля SIPO было лень переписывать стандартный код выдачи старшего бита первым, поэтому я просто перевернул выходной вектор используя цикл for (ресурсов ПЛИС это не требует, а просто переподключает провода в векторе oq[N: M] = q[M: N])

SIPO

`timescale 1ns / 1ps

module sipo #(parameter C_WIDTH = 4)(
    input  iclk,
    input  ice,
    input  id,
    output [C_WIDTH-1:0] oq,
    output ovalid
);
reg [C_WIDTH-1:0] q;
reg valid;
reg ce;

always @(posedge iclk) begin
    if (ice) begin
        q <= {q [C_WIDTH-1:0], id};
    end
end

genvar i;
generate
  for(i=0; i

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

Схема топ модуля (elaborate)

Схема топ модуля (elaborate)

Параметр разрядности входных операндов C_WIDTH, установлен в 10000. Но для отладки лучше поставить что-то около 4.

код модуля верхнего уровня

`timescale 1ns / 1ps

module top #(parameter C_WIDTH = 10000)(
    input iclk,
    input [C_WIDTH - 1 : 0]ia,
    input [C_WIDTH - 1 : 0]ib,
    input iload,
    output [C_WIDTH : 0] osum,
    output ovalid
    );
    
    wire control_oread;
    wire piso_a_oq;
    wire piso_b_oq;
    
    wire fa_oc;
    wire fa_os;
    
    wire [C_WIDTH - 1 : 0] sipo_oq;
    
    wire piso_ovalid;
    reg piso_ovalid_dff;
    wire delayed_load;
    
    full_adder fa (
        .iclk(iclk),
        .ireset(iload),
        .ia(piso_a_oq),
        .ib(piso_b_oq),
        .os(fa_os),
        .oc(fa_oc)
    );
    
    piso #(.C_WIDTH(C_WIDTH)) piso_a  ( 
        .iclk(iclk),
        .iload(iload),
        .id(ia),
        .ice(control_oread),
        .ovalid(piso_ovalid),
        .oq(piso_a_oq)
    );
    
    piso  #(C_WIDTH) piso_b (
        .iclk(iclk),
        .iload(iload),
        .id(ib),
        .ice(control_oread),
        .oq(piso_b_oq)
    );
    
    srl  #(C_WIDTH) srl_read (
        .iclk(iclk),
        .id(iload),
        .ice(1'b1),
        .oq(delayed_load)
    );
    
    sipo  # (C_WIDTH) sipo_s (
        .iclk(iclk),
        .ice(piso_ovalid_dff),
        .id(fa_os),
        .oq(sipo_oq),
        .ovalid(ovalid)    
    );
        
    flowcontrol fc (
        .iclk(iclk),    
        .iload(iload),
        .iload_delayed(delayed_load),
        .oread(control_oread) 
    );
    reg oc_dff;
    always @(posedge iclk) begin
        oc_dff <= fa_oc;
        piso_ovalid_dff <= piso_ovalid;
    end
    
    assign osum = {oc_dff, sipo_oq};
    
endmodule

Ну и куда же в нашем деле без тестбенча

Тестовое окружение

`timescale 1ns / 1ps

module tb ();
    parameter C_WIDTH = 1000;
    parameter MAX_2POW = 15;
    reg clk;
    wire [C_WIDTH : 0] osum;
    wire ovalid;
    reg [C_WIDTH -1 : 0] ia, ib;
    reg load;
    
     initial begin
        clk = 0;
        ia = 0;
        ib = 0;
        load = 0;
    end
    
    always 
        #5 clk = !clk;
     
     integer i, j;   
     always begin
     //fpga startup
     #1000
     //
        for (i = 5; i < 2**MAX_2POW; i = i + 1) begin
            for (j = 6; j < 2**MAX_2POW; j = j + 1) begin
                ia = i;
                ib = j;
                load = 1;
                #10
                load = 0;
                wait (ovalid);
                #10;
            end
        end
        
        #200;
        $finish;
    end
    
    top #(C_WIDTH) dut (
        .iclk(clk),
        .ia(ia),
        .ib(ib),
        .iload(load),
        .osum(osum),
        .ovalid(ovalid)    
    );
endmodule

Задержка в модуле получилась C_WIDTH + 3, то есть разрядность операндов + 3 такта. Уверен можно сделать лучше, чем вы самостоятельно можете и заняться.

Временная диаграмма для post-timing simulation при C_WIDTH = 4 на 250МГц приведена ниже. Как будто бы даже показывает, что все работает правильно, но скорее всего это я что-то не доглядел в тестбенче.

post-timing simulation при C_WIDTH = 4 на 250МГц

post-timing simulation при C_WIDTH = 4 на 250МГц

Ну, а теперь пара абзацев образовательного контента

1. режим синтеза out-of-context
Когда мы выставим параметр C_WIDTH больше какого-то значения, то в нашем проекте будет много портов, и количество ножек микросхемы может не хватить для запуска его имплементации.

Прикинем: пусть C_WIDTH = 1000, тогда ia, ib, osum вместе дадут 3000, а столько свободных пинов нет ни у одной микросхемы ПЛИС. Если запустить проект на имплементацию, то синтез та пройдет, а вот на размещении среда выдаст ошибку из разряда: пинов та не хватает в кристалле для размещения топа, извиняй. И как тут быть?

Здесь на помощь приходит режим синтеза out-of-context: он запрещает подключение синтезируемого подуля к ножкам ПЛИС, и не делает вставку вх/вых буферов в нетлист. На картинке пример синтеза без и в режиме out-of-context

разница результатов синтеза с out-of-context (ooc) и без него

разница результатов синтеза с out-of-context (ooc) и без него

Данный режим полезен, когда надо проверить синтезируемость модуля в ПЛИС, если не хватает портов или если вы делаете прототип асика, а сам нетлист используете как модуль, вместо вставки верилог кода.

Включить режим ooc можно в настройках синтезатора, ну, а почитать подробнее можно в UG901 Synthesis guide и здесь

Включение режима ooc  в настройка синтезатора

Включение режима ooc в настройка синтезатора

2. Как ускоряем проект

При выставлении параметра разрядности операндов C_WIDTH в большое значение (например 10000) мы будем наблюдать, что целевая тактовая частота, скажем в 250МГц ограничена сверху не столько количеством уровней логики (которых там всего 1), сколько так называемым net fanout — грубо говоря, к источнику сигнала подключено большое количество потребителей. Об этом на сообщает тайминг репорт после имплементации (хотя увидеть fanout можно уже и после синтеза)

22b0103074d01be83ac6832ff2488b63.png

Что делать в таком случае? Естественно надо уменьшить fanout и обычно для этого используется клонирование источника сигнала, то есть идея какая: 1 источник на 10000 потребителей или 2 источника на 5000 потребителей или 4 источника на 2500 потребителей на каждого.

Ограничить fanout можно несколькими способами: глобально через настройки синтезатора, локально для всех инстансов модуля для через HDL атрибуты и локально для каждого инстанса через файл проектных ограничений xdc (BLOCK_SYNTH). Все эти способы описаны в UG901 Synthesis Guide и еще отрывочно в нескольких гайдах UG906, 904, 903

На этом у меня пока всё, увидимся.

пруф

пруф

© Habrahabr.ru