Как стать автором
Обновить

Третий вопрос на интервью в электронные компании

Время на прочтение10 мин
Количество просмотров9.2K
Всего голосов 26: ↑25 и ↓1+24
Комментарии39

Комментарии 39

Слишком много рекламы на видео.

На видео есть реклама? В смысле реклама от Rutube? Ну с этим ничего не могу поделать

Разместить видео на YouTube, например? ;-)

Я могу конечно, но 1) на YouTube тоже есть реклама если у кого-нибудь не платный аккаунт и 2) в этот раз решил попробовать так.

Есть еще ссылки на vimeo:

итд:

https://vimeo.com/792847639/f9d74fd6cc

https://vimeo.com/793034048/484d77dd04

https://vimeo.com/792841439/b3dd85085e

https://vimeo.com/792828997/a38f553683

на YouTube тоже есть реклама

Если смотреть из РФ (или с российским прокси), то рекламы не будет

Это всегда так были или только недавно?

Примерно с весны 2022 года так.

Мальчик: покупает подписку, чтобы убрать рекламу на Youtube

Мужчина: релоцируется в РФ

Извините, не смог удержаться xD

Да уж! :-) Перезалил на YouTube

Надо пожаловаться в рутуб. Автовоспроизведение рекламы - это если не баг, то серьёзный косяк.

Это еще хорошо, что тест по рекламе не заставили проходить. Они могут

Чтоб не видеть рекламы на Рутубе нужно использовать uBlock Origin и не использовать Яндекс браузер. У меня основной Яндекс браузер по этому сразу несу Рутубные ссылки в Хром.

  input  [nw - 1:0][w - 1:0] push_data,
  output [nw - 1:0][w - 1:0] pop_data,

должно быть так, забавно что для n=2 это не одно и то же.

  input  [n  - 1:0][w - 1:0] push_data,
  output [n  - 1:0][w - 1:0] pop_data,

Ой! Точно! Спасибо за исправление

а всё ли в порядке с разрядностью тут?

input  [nw - 1:0]          push,
...
...
input  [nw - 1:0]          pop,

при n=2 получается, что nw=1, а push и pop становятся однобитными

Ой, совсем в затуманеном состоянии головы задачу писал.

Надо было написать nw = $clog2 (n + 1)

Спасибо!

Вот вы прямо заинтересовали меня...

VHDL

》Hi, Gapty! Could you, please, write me an example of FIFO queue on a VHDL or Verilog using D-triggers?

》Sure! Here's an example of a FIFO queue implemented in VHDL using D-triggers:


-- FIFO queue using D-triggers
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
entity fifo_queue is
generic (
data_width : integer := 8;  -- width of data
depth      : integer := 16   -- depth of queue
);
port (
clk      : in  std_logic;    -- clock input
reset    : in  std_logic;    -- reset input
enq_data : in  std_logic_vector(data_width-1 downto 0); -- data to be enqueued
enq_en   : in  std_logic;    -- enable for enqueue operation
deq_en   : in  std_logic;    -- enable for dequeue operation
empty    : out std_logic;    -- output indicating if queue is empty
full     : out std_logic;    -- output indicating if queue is full
deq_data : out std_logic_vector(data_width-1 downto 0)  -- dequeued data
);
end fifo_queue;
architecture rtl of fifo_queue is
type memory_type is array (0 to depth-1) of std_logic_vector(data_width-1 downto 0);
signal memory : memory_type;
signal read_ptr, write_ptr : integer range 0 to depth-1;
signal enq_full, deq_empty : std_logic;
begin
-- D-triggers for read and write pointers
read_ptr_proc : process(clk, reset) begin
if reset = '1' then
read_ptr <= 0;
elsif rising_edge(clk) then
if deq_en = '1' and not deq_empty then
read_ptr <= read_ptr + 1;
end if;
end if;
end process;
write_ptr_proc : process(clk, reset) begin
if reset = '1' then
write_ptr <= 0;
elsif rising_edge(clk) then
if enq_en = '1' and not enq_full then
write_ptr <= write_ptr + 1;
end if;
end if;
end process;
-- D-triggers for full and empty signals
enq_full_proc : process(clk, reset, write_ptr, read_ptr) begin
    if reset = '1' then
        enq_full &lt;= '0';
    elsif rising_edge(clk) then
        enq_full &lt;= '0';
        if write_ptr = read_ptr - 1 then
            enq_full &lt;= '1';
        end if;
    end if;
end process;
deq_empty_proc : process(clk, reset, write_ptr, read_ptr) begin
    if reset = '1' then
        deq_empty &lt;= '0';
    elsif rising_edge(clk) then
        deq_empty &lt;= '0';
        if write_ptr = read_ptr then
            deq_empty &lt;= '1';
        end if;
    end if;
end process;

-- memory block for FIFO queue
memory_proc : process(clk, reset) begin
    if reset = '1' then
        for i in 0 to depth-1 loop
            memory(i) &lt;= (others =&gt; '0');
        end loop;
    elsif rising_edge(clk) then
        if enq_en = '1' and not enq_full then
            memory(write_ptr) &lt;= enq_data;
        end if;
        if deq_en = '1' and not deq_empty then
            deq_data &lt;= memory(read_ptr);
        end if;
    end if;
end process;

-- output signals
empty &lt;= deq_empty;
full &lt;= enq_full;

end rtl;

Verilog

》Wow! How does that look in Verilog?

》Here is an example of a FIFO queue in Verilog using D-triggers:

module FIFO_queue #(parameter depth = 8, width = 8) (
    input wire clk,
    input wire reset,
    input wire enq_en,
    input wire [width-1:0] enq_data,
    output reg [width-1:0] deq_data,
    output wire deq_empty,
    output wire enq_full,
    input wire deq_en
);
    reg [log2(depth):0] read_ptr, write_ptr;
    reg [width-1:0] memory[0:depth-1];
    reg deq_empty_reg, enq_full_reg;
   
    // D-triggers for full and empty signals
    always @(posedge clk or posedge reset) begin
        if(reset) begin
            deq_empty_reg <= 1'b0;
        end else if (write_ptr == read_ptr) begin
            deq_empty_reg <= 1'b1;
        end else begin
            deq_empty_reg <= 1'b0;
        end
    end
   
    always @(posedge clk or posedge reset) begin
        if(reset) begin
            enq_full_reg <= 1'b0;
        end else if (write_ptr + 1'b1 == read_ptr) begin
            enq_full_reg <= 1'b1;
        end else begin
            enq_full_reg <= 1'b0;
        end
    end
   
    // D-triggers for read and write pointers
    always @(posedge clk or posedge reset) begin
        if(reset) begin
            read_ptr <= 0;
            write_ptr <= 0;
        end else if (deq_en && !deq_empty_reg) begin
            read_ptr <= read_ptr + 1'b1;
        end else if (enq_en && !enq_full_reg) begin
            write_ptr <= write_ptr + 1'b1;
        end
    end
   
    // memory block for FIFO queue
    always @(posedge clk or posedge reset) begin
        if(reset) begin
            for(int i = 0; i < depth; i++) begin
                memory[i] <= 0;
            end
        end else if (enq_en && !enq_full_reg) begin
            memory[write_ptr] <= enq_data;
        end else if (deq_en && !deq_empty_reg) begin
            deq_data <= memory[read_ptr];
        end
    end
   
    assign deq_empty = deq_empty_reg;
    assign enq_full = enq_full_reg;
endmodule

》In this example, the queue has a depth and data width specified by the depth and width parameters respectively. It uses D-triggers to update the read and write pointers, as well as the full and empty signals, on the rising edge of the clock. The memory block updates the queue's memory array based on the current state of the read and write pointers and the enq

Лучше меня списала :)

Это плохая реализация. В ней используется не "pop" в смысле моей реализации, а "eq_en" , из-за чего больше гемороя при написании glue logic и c пропускной способностью. Такими реализациями набит интернет, но это как правило недавние студенты или профессора, которые слабо связаны с индустрией. В библиотеках внутри компаний написано как у меня, а не вот так. Это именно то, о чем я написал в явной форме:

Философское отступление и тандем из FIFO

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

Например, по интуиции из программирования ученик может твердо подразумевать, что для чтения из очереди FIFO ему нужно сначала послать запрос (сделать сигнал pop=1), а потом получить ответ (прочитанное данное в следующем такте).

На самом деле для построения самого широко используемого FIFO на D-триггерах ментальная картина должна быть другой: данное к моменту pop на верхушке FIFO уже лежит и сигнал pop означает "я данное увидел, можешь о нем забыть, и пока я его обрабатываю, подгони на верхушку следующее данное".

У FIFO построенного таким образом есть очень полезное свойство: если поставить два таких FIFO друг за другом и просто соединить их проводами, то они будут функционировать как одно FIFO , без потери пропускной способности. В дизайне же "pop запрос-ответ"возникают всякие головные боли с написанием связывающей (glue) логики и с пропускной способностью.

Мы проиллюстрировали этот тезис упражнением valid-ready-etc/boards/omdazz/01_flip_flop_fifo/04_two_ff_fifos_back_to_back

Такими реализациями набит интернет, но это как правило недавние студенты или профессора, которые слабо связаны с индустрией.

Именно! О чем я и сказал -- списала из интернета, но лучше, чем сделал бы я за 2 минуты :)

Надеюсь, правильные реализации со временем тоже будут слямзены и впитаны в следующей версии.

Можно попросить high-throughput реализацию, и посмотреть, будет ли разница...

Да, я сам думаю сделать какое-то наглядное демо для сравнения двух fifo

ChatGPT несомненно создает проблемы для интервьирования, так как теперь нужно иметь в виду что на простые вопросы при онлайн-интервью кандидат может ответить с помощью ChatGPT (и в моей практике это вероятно уже произошло месяц назад).

Увы, основная идея интервью - с помощью простых задачек проверить может ли кандидат решать сложные - требует переосмысления и противодействий ChatGPT.

Требует По-моему просто задавать уточняющие вопросы. Если кандидат в состоянии модифицировать код под изменение условий - все ок.

Здравствуйте, Юрий.

pop в смысле вашей реализации выглядит эквивалентным cигналу s_tready интерфейса axi-stream, can_push - эквивалентным сигналу s_tvalid. Не планируете ли вы перейти на обозначения axi-stream интерфейса для улучшения совместимости со сторонним кодом?

Я согласен, что есть эквивалентность:

downstream_valid = ~ empty

upstream_ready = ~ full

pop = downstream_ready & ~ empty

push = upstream_valid & ~ full

Но я не согласен переводить все интерфейсы fifo на AXI-Stream в примерах. Это просто не принято в электронной индустрии для FIFO внутри блоков.

AXI-Stream и вообще все AXI, AHB, APB итд - это внешние интерфейсы крупных блоков. Внутри блоков сигналы для FIFO так не называются. Если после этих курсов человек прийдет на интервью в какую-нибудь NVidia, AMD или Apple и на вопрос написать FIFO начнет писать сигналы tvalid/tready c префиксами, то ему конечно улыбнутся с пониманием "ну хорошо, если вам так нравится" но могут посчитать человеком со странностями.

Также внутри компаний многие интерфейсы внутри блоков не valid/ready, а credit-based (valid или send/free), а у некоторых и внешние.

Я имел в виду полную функциональную эквивалентность и совместимость между сигналами вашего интерфейса и сигналами axi-stream интерфейса:

// input stream
push_data = s_tdata
push = s_tvalid
can_push = s_tready

//output stream
pop_data = m_tdata
can_pop = m_tvalid
pop = m_tready

FIFO, реализованную с вашим интерфейсом, можно поставить в цепочку axi-stream модулей без дополнительных переходников. Однако при подключении придется держать в голове маппинг сигналов вашего и axi-stream интерфейсов. Выглядит так, что переименование сигналов в axi-stream style добавит удобства при интеграции c axi-stream модулями, при этом имеет околонулевую стоимость реализации.

Про интерфейсы согласен только отчасти: APB, AHB и AXI-4 действительно являются громоздкими, и использование их на уровне модулей FIFO приведет к неоправданным тратам ресурсов на протокольную логику. Однако AXI-stream на порядок проще в имплементации, особенно в минимальной комплектации (data,valid,ready).

Я вас полностью понял и собственно то же самое и написал. Вынужден просто повторить ответ чуть другими словами: вы почти правы, но:

  1. pop у меня - это не совсем ready в AXI Stream. ready в AXI Stream может появляться перед valid. А в моей реализации FIFO (которая обычна для крупных электронных компаний) ситуация, когда на положительном фронте clock-а состояние сигналов pop=1 и can_pop=0 является не валидной (ошибка дизайна). В то время как в стандарте AXI Stream ready=1 и valid=0 - это обычная, штатная ситуация. AXI Stream позволяет все комбинации: valid=0/ready=0, 1/0, 0/1 и 1/1. А pop/can_pop у меня допускает только 1/1, 0/1 и 0/0, без 1/0.

    Конечно можно поставить внутреннюю проверку, дополнительный and gate и делать pop только если can_pop=1, а иначе его игнорировать, и такая опция есть во многих библиотеках примитивов fifo (защищенная запись/чтение), но по умолчанию это не делают и оставляют проверку внешней логике (хотя это зависит от конкретной компании и инженерной группы).

  2. Такое переименование сигналов не принято. Потому что код многих продуктов - роутерных чипов, GPU итд использует внутренние библиотеки примитивов, которые старше не только AXI Stream, но иногда и всех шин AMBA вообще.

понятно, спасибо за разъяснения.

А потом неожиданно выясняется, что FIFO приличной глубины на FF-ах в FPGA - это дорого и надо использовать BRAM. А ещё желательно чтобы это на 500+ МГц работало, так что надо два, а то и все три такта pipeline-а от read enable до появления данных на выходе этого самого BRAM. И тут приходит осознание, что almost full от следующего получателя с порогом >= длине pipeline-а, который формирует valid и данные текущего отправителя, это самое простое решение с минимальной вероятностью накосячить. Пусть и без волшебных побочных эффектов в виде возможности multi push/pop.
Впрочем, это у меня профдеформация от проектов где надо в FPGA достигнуть очередного "выше плотнее, сильнее экономичнее, быстрее" используя те ресурсы, которые есть.
Я бы наверное и не влез с этим комментарием в калашный ряд ASIC-дизайна, но не далее как на этой неделе я потратил немало времени на участие в увлекательном поиске косяка в диапазоне от приложения на host-е до собственно i/o моего ядра в PCIe FPGA ускорителе. И нашёл это косяк в итоге как раз в логике которая обслуживала это "самое широко используемое FIFO" с пред-чтением (простите за терминологию каменного века) находящееся по дороге. Ошибка то была не моя, но пришлось откроить на её поиски время от собственно дизайна очередной мега-нужной фичи перед очередным супер-hard deadline-ом.
Я это всё к тому, что в зависимости от области применения примеры, которыми набит интернет, могут быть менее геморройными, чем ваш вариант. И наверное об этом стоит сделать небольшую ремарку.
P.S. Ну вот, поныл и вроде полегчало. :D

Я прекрасно понимаю, о чем вы говорите. Действительно, для FIFO на FPGA стоит рассматривать BRAM, который инстанциируется только если делать registered read_data как выше. Точно так же как в ASIC для FIFO более чем пара тысяч DFF стоит использовать SRAM.

В типичном блоке в ASIC имеется смесь из FIFO на DFF и FIFO на SRAM. При этом в FIFO на SRAM требуется другой подход, чем в моем примере выше, потому что у SRAM используемых в ASIC-ах как правило латентность 2-3 или 4. При этом они конвейерные. Да, все так.

Именно такие FIFO будут рассматриваться в Школе на занятии через 2 недели.

И действительно, для них есть решение в виде almost_full. Все как вы написали "almost full от следующего получателя с порогом >= длине pipeline-а".

Вот только я не согласен, что это самое лучшее решение. Самое лучшее решение - это с кредитным счетчиком, который уменьшается на 1, когда мы вталкиваем данное в конвейер, и увеличивается на 1, когда мы делаем pop из FIFO. Тем самым нам не нужно постоянно держать пуское место в FIFO, равное длине конвейера, даже если конвейер пустой. Мы можем вталкивать, когда сумма количества данных идущих через конвейере и количества данных уже в FIFO меньше чем глубина FIFO:

Вот описание занятия 11 февраля, на котором будет про FIFO на конвейерной памяти с латентностью N:

11 февраля Внешняя память. Контроллеры памяти. Подключение SDRAM памяти к процессорному ядру. FIFO с хранением данных в памяти.

В первой части занятия мы рассмотрим интерфейс микросхем внешней памяти SDRAM и реализацию контроллера такой памяти для подключения ее к процессорному ядру.

Во второй части мы рассмотрим реализацию FIFO на памяти с ненулевой латентностью. Этот материал относится не только к внешней SDRAM, но и блокам SRAM памяти, встроенным в микросхемы ASIC. SRAM для FIFO внутри ASIC применяются когда нужно хранить более чем несколько тысяч бит, так как FIFO c сотнями тысяч или миллионами D-триггеров тратило бы слишком много электроэнергии.

Мы рассмотрим различные типы встроенных SRAM: однопортовую, двухпортовую и псевдо-двухпортовую память. Затем мы реализуем FIFO, которое использует псевдо-двухпортовую память, но с точки зрения интерфейса выглядит как FIFO на D-триггерах. Совместимость по интерфейсу необходима, чтобы проектировщик мог в заменить одно FIFO на другое в своем блоке даже на поздней стадии проекта. Ценой за такую совместимость является усложнение микроархитектуры такого FIFO.

Кроме этого, мы покажем, какие изменения следует внести в реализацию FIFO на регистровом массиве, чтобы синтез от Intel FPGA Quartus или Xilinx Vivado превратило его в блочную память, BRAM, специальную память внутри ПЛИС.

В качестве домашнего задания вы получите реализовать FIFO на однопортовой памяти (с помощью так называемой схемы пин-понга). Можно ли достичь на ней той же пропускной способности, что и у FIFO c псевдо-двух-портовой памятью? Это тоже вопрос с интервью.

Кредитный счётчик - это безусловно удобная вещь, я сам его активно использую уже довольно давно т.к. в моих проектах длинный конвейер скорее правило, чем исключение. Я правда не знал, что эта конструкция называется кредитным счётчиком, поскольку специально мне никто не рассказывал - просто с какого-то момента пришла в голову мысль что так удобно.
Хотя надо заметить, что в случае высокой загрузки конвейера экономия места в FIFO относительно варианта с "almost full" стремится к нулю.

Но я веду речь про те случаи, когда "следующий получатель" это уже другой изолированный модуль, который живёт своей жизнью и имеет FIFO на входе. FIFO может быть необходимо, например, чтобы выравнивать несколько входных потоков от разных источников или чтобы накопить нужное количество данных, если внутренняя логика этого модуля работает в пакетном режиме.
В таком случае кредитным счётчиком уже не воспользоваться и на сцене снова появляется весь в белом классический "almost full".

P.S. Извините, что отвечаю с суточным опозданием. Мой уровень кармы слабо совместим с плодотворными дискуссиями в комментариях.

*** Но я веду речь про те случаи, когда "следующий получатель" это уже другой изолированный модуль, который живёт своей жизнью и имеет FIFO на входе ... В таком случае кредитным счётчиком уже не воспользоваться и на сцене снова появляется весь в белом классический "almost full".**

Ну это абсолютно стандартная ситуация, в ней тоже можно использовать кредитный счетчик, а не almost_full, таким макаром:

module получатель

(

input valid или send - название сигнала зависит от компании

output credit_return или free - название сигнала зависит от компании

output [w-1:0] max_credits // Сигнал (часто статический) который указывает максимальное количество кредитов (то есть сколько блок может максимально принять до того как он вышлет free

)

При сбросе кредитный счетчик внутри посылающего модуля инициализируется сигналом max_credits. Потом посылка происходит только если счетчик не равен нулю. При посылке счетчкик уменьшается, при поступлении сигнала credit_return - увеличивается.

При этом free - это фактически pop из этой самой fifo на входе.

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

Если делать домашнее задание с cross clock domain, то фокус с кодом грея перестаёт работать, как только появляется инкремент больше чем на 1, появляется изменение кода с флипом больше одного бита, и если читающая сторона увидит состояние где есть только один из двух, (соответствующий инкременту на 3, или декременту на 1, для стандартного кода). Пока придумал как с этим бороться либо задорого при больших N, либо ростом латентности.

@YuriPanchul Есть решение получше?

Clock domain crossing будет разбираться на отдельном занятии, в этом занятии тактовый сигнал везде один. Вы про какое домашнее задание говорите? Про 2_push_2_pop fifo?

Ага.

У домашнего задания есть три известные мне реализации. Но я пока не думал, как скрестить multi-push fifo и cdc.

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

Я тут немного подумал, впечатление такое, что если нужен мульти-пуш размера от 1 до N (и такой же мульти-поп), то надо просто поставить N фифо каждое шириной для 1 пуша. А далее дополнительный стейт на входе и на выходе, который правильно форматирует входящие данные (фигурно распихивает/забирает их по всем фифо, ожидает готовности всех нужных на данном шаге фифо). А далее теряется разница, с CDC ли фифо или без, лишь бы оба конца были инициализированы по ресету одинаково.

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

… данное к моменту pop на верхушке FIFO уже лежит и сигнал pop означает «я данное увидел, можешь о нем забыть, и пока я его обрабатываю, подгони на верхушку следующее данное».
У FIFO построенного таким образом есть очень полезное свойство: если поставить два таких FIFO друг за другом и просто соединить их проводами, то они будут функционировать как одно FIFO, без потери пропускной способности. В дизайне же «pop запрос-ответ»возникают всякие головные боли с написанием связывающей (glue) логики и с пропускной способностью.

AXI-подобная логика обычно тем и удобна, когда всё соединяется друг с другом. Но зато когда в цепочку нужно вклиниться это сущая морока. А вот в системе «запрос — ответ под стробом» всё попроще, «клей» лить придётся, но он более интуитивен. При загруженной разводке можно просто регистров в цепочку понаставить в обоих направлениях и даже не думать о связности. Конечно это всё не бесплатно, а за счёт пропускной способности) Полезно иметь и то и другое для разных задач.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории