11. Память на ПЛИС

Как и в большинстве вычислительных устройств, в ПЛИС есть своя иерархия памяти, построенная вокруг всё того же поиска баланса между скоростью доступа и объёмом (на самом деле ценой).

Нас будет интересовать память, которая доступна на кристале. В основном, это регистры и блочная память (B-SRAM).

Регистры и B-SRAM в ПЛИС
Рисунок 1. Регистры и B-SRAM в ПЛИС: CFU — configurable functional unit, CRU — configurable routing unit, CLS — configurable logic slice

Естественно, можно подключать и внешнюю (в том числе энергонезависимую) память, но это уже зависит от платы (в меньшей мере от чипа). Часто доступны DDR, Flash, при желании и какой-нибудь съёмный носитель можно подключить. Для ускорителей характерно использование HBM.

Block SRAM

Block SRAM (B-SRAM, иногда просто SRAM) — энергозависимая память на кристале, организованная не очень большими блоками (десятки килобит), суммарным объёмом порядка сотен килобайт. Например, для наших плат.

  • Для Tang Primer 25K: 56 блоков по 18 килобит (порядка 120 килобайт суммарно)

  • Для Tang Mega 138K Pro Dock: 340 блоков по 18 килобит (порядка 760 килобайт суммарно)

Каждый блок может быть сконфигурирован отдельно или включён в состав хранилища, организованного из нескольких блоков. На что важно обратить внимание.

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

  • В блок могут быть загружены какие-либо (задаваемые пользователем) данные при инициализации.

У Gowin есть несколько режимов работы BSRAM:

  • Single port — один порт для чтения и записи.

  • Dual port — два независимых порта: оба могут использоваться и для чтения, и для записи.

  • Semi-Dual port — два порта: один для чтения, другой для записи.

  • Read Only — память, инициализирующаяся изначально и доступная только для чтения.

Блоки можно подключать вручную, но производители рекомендуют использовать возможности соответствующих САПР. Посмотреть, как это делается в нашем случае можно в инструкции.

Пример работы с памятью.

Рассмотрим простейший пример, в котором воспользуемся однопортовой BSRAM. Задача такая: на вход каждый такт поступает пиксель картинки, мы сначала записываем её в память, а после того, как место кончится, с начала выдаём на выход те пиксели, которые были записаны, а после записываем на его место новый пиксель. То есть реализуем простейший line buffer, который потребуется вам в дальнейшем для реализации свёртки изображения.

При работе с памятью больше всего затруднений вызывает именно аккуратная работы с задержками: запись происходит с задержкой, результат чтения (если использовать pipeline) происходит тоже только на следующий такт. После заполнения буфера, нам нужно будет читать пиксель из памяти, и только после этого перезаписывать. Это займёт как минимум два такта (вообще, три, если бы нам гипотетически приходилось бы читать из той же ячейки, в которую мы писали), что при поступлении картинки потактово вызовет неизбежные потери пикселей, подумайте, как можно подходить к решению этой проблемы, мы же перейдём к описанию кода.

Предварительно изучите, как подключать память.
src/11-bsram/line_buffers/line_buffer.sv
`timescale 1ns / 1ps (1)
module line_buffer #(
  parameter int IMAGE_WIDTH = 640,
  parameter int PIXEL_WIDTH = 8
)(
  input  logic clk,
  input  logic rst,
  input  logic [PIXEL_WIDTH - 1:0] pixel_in,
  output logic [PIXEL_WIDTH - 1:0] pixel_out,
  output logic ready_to_write (2)
);

logic wre;
logic[1:0] state;
logic[9:0] address;
logic ce = 1;
logic oce = 1;


parameter int FILLING = 0; (3)
parameter int WRITE = 1;
parameter int READ = 2;

logic [PIXEL_WIDTH-1:0] din;


Gowin_SP sp( (4)
  .dout(pixel_out),
  .clk(clk),
  .oce(oce),
  .ce(ce),
  .reset(rst),
  .wre(wre),
  .ad(address),
  .din(din)
);



always_ff @(posedge clk) begin
  if (rst) begin
    address <= 0;
    state <= FILLING;
    wre <= 1;
    din <= pixel_in;
    ready_to_write <= 1;
  end
  unique case(state)
    FILLING: (5)
    begin
      din <= pixel_in;
      if (address == IMAGE_WIDTH - 1) begin
        state <= READ;
        address <= 10'd0;
        wre <= 0;
        ready_to_write <= 0;
      end
      else address <= address + 10'd1;
    end
    READ: (6)
    begin
      state <= WRITE;
      wre <= 1;
      ready_to_write <= 1;
    end
    WRITE: (7)
    begin
      state <= READ;
      wre <= 0;
      ready_to_write <= 0;
      din <= pixel_in;
      if (address == IMAGE_WIDTH - 1) begin
        address <= 10'd0;
      end
      else address <= address + 10'd1;
    end
  endcase
end

endmodule
1 В подключенном нами $FPGA_TOOLS/simlib/gw1n/prim_sim.v есть timescale, для корректной работы, нам тоже это надо прописать. Этот параметр важен только для симуляции и задаёт первым параметром единицу времени для симуляции (когда задаём задержку через #delay), а вторым — точность, с которой происходит симуляция.
2 Мы будем говорить другим, когда мы готовы писать в память: когда происходит чтение, то пока не готовы.
3 Сама логика будет реализована через знакомый нам автомат Мили.
4 Собственно, подключаем модуль с памятью.
5 В самом начале пишем в память, пока можем. После переходим в состояние для чтения и меняем соответствующий флаг (wre ⇐ 0).
6 Читаем из памяти и переходим в состояние для записи, не меняя адрес. Меняем флаг для записи wre ⇐ 1.
7 После записи, переходим в состояние чтения, поменяв адрес, по которому будем читать.
Очень рекомендуем посмотреть, что происходит потактово через gtkwave.
Обязательно порешайте упражнения.